Touchscreen calibration and porting to version 8

Is there a touchscreen calibration sample for lvgl? I found some code, I’m not allowed to post links here, but you can find it when you search in Google for “TP_CAL_STATE_INIT”, tpcal.c. But it doesn’t compile with version 8. How can I change it to compile it for version 8? Is there a guide how to replace the old functions? For example looks like lv_btn_set_style is not available anymore, but I guess version 8 has button styles?

The original code was pretty horrible, with lots of code duplications. I wrote a new version:

#include "tpcal.h"
#include <stdio.h>
#include <stdlib.h>
#include "src/lvgl.h"

// calibration coefficients
float alphaX;
float betaX;
float deltaX;
float alphaY;
float betaY;
float deltaY;

Callback onCalibrationEnd;

static lv_point_t p[3];
static int step;
static lv_obj_t *label_main;
static lv_timer_t* timer;
static lv_obj_t* circle;

const char* helpText[] = { "Click the circle in upper left-hand corner\nand hold down until the next circle appears.",
                           "Click the circle in uppper right-hand corner\nand hold down until the next circle appears.",
                           "Click the circle in lower right-hand corner\nand hold down until the text disappears."
                         };
static lv_point_t circlePos[3];

static int currentCenterX;
static int currentCenterY;

void centeredGrowing(lv_obj_t* obj, int32_t size)
{
  lv_obj_set_size(obj, size, size);
  lv_obj_set_pos(obj, currentCenterX - size / 2, currentCenterY - size / 2);
}

void addCircle(int centerX, int centerY)
{
  if (circle) lv_obj_del(circle);

  // create style
  lv_style_t* style = malloc(sizeof(lv_style_t));
  lv_style_init(style);
  lv_style_set_radius(style, 10);
  lv_style_set_bg_opa(style, LV_OPA_COVER);
  lv_style_set_bg_color(style, lv_color_hex(0));

  // create object for drawing a circle, set style, and remove clickable flag
  circle = lv_obj_create(lv_scr_act());
  lv_obj_remove_style_all(circle);
  lv_obj_add_style(circle, style, 0);
  lv_obj_clear_flag(circle, LV_OBJ_FLAG_CLICKABLE);

  // initial size is 0
  lv_obj_set_size(circle, 0, 0);
  currentCenterX = centerX;
  currentCenterY = centerY;
  lv_obj_set_pos(circle, centerX, centerY);

  // animation for increasing the size of the circle
  static lv_anim_t a;
  lv_anim_init(&a);
  lv_anim_set_var(&a, circle);
  lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t) centeredGrowing);
  lv_anim_set_values(&a, 0, 20);
  lv_anim_set_time(&a, 300);
  a.act_time = 0;
  lv_anim_start(&a);
}

void nextStep(int x, int y)
{
  // save and show coordinates from last step
  if (step > 0) {
    p[step - 1].x = x;
    p[step - 1].y = y;
  }

  if (step == 3) {
    lv_obj_del(circle);
    circle = NULL;
    lv_obj_del(label_main);
    lv_timer_del(timer);

    // calculate coefficients based on the algorithms described here:
    // https://www.ti.com/lit/an/slyt277/slyt277.pdf
    float x1 = circlePos[0].x;
    float y1 = circlePos[0].y;
    float x2 = circlePos[1].x;
    float y2 = circlePos[1].y;
    float x3 = circlePos[2].x;
    float y3 = circlePos[2].y;
    float xs1 = p[0].x;
    float ys1 = p[0].y;
    float xs2 = p[1].x;
    float ys2 = p[1].y;
    float xs3 = p[2].x;
    float ys3 = p[2].y;

    float delta = (xs1 - xs3) * (ys2 - ys3) - (xs2 - xs3) * (ys1 - ys3);

    float deltaX1 = (x1 - x3) * (ys2 - ys3) - (x2 - x3) * (ys1 - ys3);
    float deltaX2 = (xs1 - xs3) * (x2 - x3) - (xs2 - xs3) * (x1 - x3);
    float deltaX3 = x1 * (xs2 * ys3 - xs3 * ys2) - x2 * (xs1 * ys3 - xs3 * ys1) + x3 * (xs1 * ys2 - xs2 * ys1);

    float deltaY1 = (y1 - y3) * (ys2 - ys3) - (y2 - y3) * (ys1 - ys3);
    float deltaY2 = (xs1 - xs3) * (y2 - y3) - (xs2 - xs3) * (y1 - y3);
    float deltaY3 = y1 * (xs2 * ys3 - xs3 * ys2) - y2 * (xs1 * ys3 - xs3 * ys1) + y3 * (xs1 * ys2 - xs2 * ys1);

    alphaX = deltaX1 / delta;
    betaX = deltaX2 / delta;
    deltaX = deltaX3 / delta;

    alphaY = deltaY1 / delta;
    betaY = deltaY2 / delta;
    deltaY = deltaY3 / delta;

    onCalibrationEnd();
  } else {
    // show help text for next step
    lv_label_set_text(label_main, helpText[step]);

    // show circle
    addCircle(circlePos[step].x, circlePos[step].y);
    step++;
  }
}

void inputPolling(lv_timer_t * timer)
{
  // get input device and read touch position
  lv_indev_t* indev = lv_indev_get_next(NULL);
  lv_indev_data_t data;
  indev->driver->read_cb(indev->driver, &data);

  // sample touch position after half a second delay after pressed, to get stable coordinates
  static int lastState = LV_INDEV_STATE_REL;
  static int counter = 0;
  if (lastState == LV_INDEV_STATE_REL && data.state == LV_INDEV_STATE_PR) {
    counter = 5;
  }
  if (counter) {
    counter--;
    if (counter == 0) nextStep(data.point.x, data.point.y);
  }
  lastState = data.state;
}

void tpcalCreate(Callback fun)
{
  onCalibrationEnd = fun;

  // set calibration coefficients to identy
  alphaX = 1;
  betaX = 0;
  deltaX = 0;
  alphaY = 0;
  betaY = 1;
  deltaY = 0;

  timer = lv_timer_create(inputPolling, 50,  NULL);

  // create help text label
  label_main = lv_label_create(lv_scr_act());
  lv_obj_set_width(label_main, LV_HOR_RES);
  lv_obj_set_pos(label_main, 0, LV_VER_RES - 50);
  lv_obj_set_style_text_align(label_main, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);

  // initialize reference positions
  circlePos[0].x = 50;
  circlePos[0].y = 50;
  circlePos[1].x = LV_HOR_RES - 50;
  circlePos[1].y = 50;
  circlePos[2].x = LV_HOR_RES - 50;
  circlePos[2].y = LV_VER_RES - 50;

  // show first circle and help text
  step = 0;
  nextStep(0, 0);
}

PS: there are some memory leaks, but I don’t care :slight_smile:

It assumes that you use the coefficients in your touchscreen driver as well, for example like this:

bool my_touchpad_read( lv_indev_drv_t * indev_driver, lv_indev_data_t * data )
{
  if (ts.touched()) {
    lastTouched = true;
    TS_Point p = ts.getPoint();

    float xs = p.x;
    float ys = p.y;
    int x = alphaX * xs + betaX * ys + deltaX;
    int y = alphaY * xs + betaY * ys + deltaY;
    
    data->state = LV_INDEV_STATE_PR;

    /*Set the coordinates*/
    data->point.x = x;
    data->point.y = y;
    lastX = x;
    lastY = y;
  } else {
    lastTouched = false;
    data->state = LV_INDEV_STATE_REL;
  }

  return false;
}


void lvgl_init()
{
  lv_init();
  lv_disp_draw_buf_init( &draw_buf, buf, NULL, screenWidth * 10 );

  /*Initialize the display*/
  static lv_disp_drv_t disp_drv;
  lv_disp_drv_init( &disp_drv );
  /*Change the following line to your display resolution*/
  disp_drv.hor_res = screenWidth;
  disp_drv.ver_res = screenHeight;
  disp_drv.flush_cb = my_disp_flush;
  disp_drv.draw_buf = &draw_buf;
  lv_disp_drv_register( &disp_drv );

  /*Initialize the (dummy) input device driver*/
  static lv_indev_drv_t indev_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 );
}

The calibration function also provides a callback to show the main GUI. Here is an example for the Arduino environment, which shows the calibration screen when started first, and then stores it in the EEPROM:

#define VALID_CALIBRATION_MARKER 42

// write a float value at the specified address into the EERPOM
void writeFloat(int address, float value) {
  char* ptr = (char*) &value;
  for (int i = 0; i < 4; i++) EEPROM.write(address + i, ptr[i]);
}

// read a float value from the specified addres from the EEPROM
float readFloat(int address) {
  float value;
  char* ptr = (char*) &value;
  for (int i = 0; i < 4; i++) ptr[i] = EEPROM.read(address + i);
  return value;
}

// save the touchscreen calibration values
void saveCalibration() {
    EEPROM.write(0, VALID_CALIBRATION_MARKER);
    writeFloat(1, alphaX);
    writeFloat(5, betaX);
    writeFloat(9, deltaX);
    writeFloat(13, alphaY);
    writeFloat(17, betaY);
    writeFloat(21, deltaY);
    startApplication();
}

void setup() {
  // init LCD
  SSD1963_Initial();

  // init LVGL library
  lvgl_init();

  // test if the touchscreen sensor is calibrated
  if (EEPROM.read(0) == VALID_CALIBRATION_MARKER) {
    // load calibration from EEPROM
    alphaX = readFloat(1);
    betaX = readFloat(5);
    deltaX = readFloat(9);
    alphaY = readFloat(13);
    betaY = readFloat(17);
    deltaY = readFloat(21);
    startApplication();
  } else {
    // run calibration procedure and save calibration data to EEPROM
    tpcalCreate(saveCalibration);
  }
}

startApplication is called after the calibration, or after reset, if already calibrated, and can show the main GUI.

It is my first LVGL program, comments for improvement are welcome. And feel free to add it to the github repository.

PS: I didn’t find an easy way to capture all events. This is why I used the timer for polling the input driver directly. Might be useful if it would be possible to add a global event hook, as for example Qt or GDI of win32 allows it, if it is not already implemented and I missed it.

The header file:

#ifndef TPCAL_H
#define TPCAL_H

// calibration coefficients
extern float alphaX;
extern float betaX;
extern float deltaX;
extern float alphaY;
extern float betaY;
extern float deltaY;

typedef void (*Callback)();

void tpcalCreate(Callback fun);

#endif

very nice new tpcal.
How did you compile and link and use it?

Thanks Pepito

Thanks. I used it with Arduino, with a Teensy 4.1. Here is how it looks like:

I integrated the latest LVGL version 8 library into the project in the src directory, and changed all include directives, always easier to have all code in one project.

@frankbuss
Would you be interested in adding it as an “extra” widget?

It is not a widget, but might be useful in examples, if someone wants to add it.

True, but it’s also more than an example. If it were a “widget” it would have its own page in the docs and probably got more visibility.

Maybe we could have an “apps” folder in src/extra?

@embeddedt what do you think?

1 Like

I think it makes more sense for it to be an example, not a widget. It can still have its own page in the documentation.

Perhaps, once the screen is calibrated, there could be an extra utility function (void tpcal_adjust_coords(lv_indev_data_t *data)) that could be called at the end of someone’s read_cb function. That way, the calibration logic could be independent of the actual touchscreen.

Okay, I don’t insist to make it a widget.

Then we should have an examples/indev folder, right?
We could have group-related examples there too.
(BTW, could we add keyboard and mouse wheel support to the example iframes?)

1 Like

I’ve added it to lv_sim_emscripten so it should be visible when the docs are rebuilt next.

There is a limitation. The way the SDL keyboard driver works, it seems that it doesn’t handle key rollover correctly. This causes keys to be dropped when typing rapidly.

I am a pretty fast typist, so if I type “test”, but I press the “e” before the “t” is fully released, LVGL only seems to register the “t”.

If you try it on a text box in lv_demo_widgets and then try it in the browser search bar, I think you will see what I mean.

Great, thanks! Now the LVGL examples are rebuilt too. It works well with the mouse wheel but unfortunately the keyboard events are lost. See Text area (lv_textarea) — LVGL documentation

I am learning LVGL, and today I almost completed my touchscreen_cal component on my ESP32 hardware. I found it difficult not having up-to-date examples of LVGL v8 code, but I struggled through. It was inspired by the old TPCAL code, but is a complete rewrite, so I have placed my own copyright with permissive license.

I’d very much like your feedback. My code still needs a few things, like the ability to retry, access to the untransformed touchscreen coordinates, saving in NVRAM and/or a callback to return the new cal data.

I’d like someone who’s experienced with LVGL (v8 in particular, which is new) to review the code, and to answer a couple of layout sequencing questions I came across while making it work. In particular, two things:

  • For a new object, lv_obj_get_x() does not return what lv_obj_set_x just set().
  • Getting the size of a piece of text so you can manage its position seems to be a black art.

Anyhow, here: GitHub - cjheath/lvgl_touchscreen_cal: A calibration screen for touchscreen geometry to work with LVGL v8

Hi,

for the layout issue take look at this part of the docs:
https://docs.lvgl.io/master/overview/coords.html#postponed-coordinate-calculation

I’ve just added updated the comments of the coordinate get functions with a note about it.

Saving the coordinates into non-volatile memory is very platform-specific and LVGL has no abstraction for this. IMO the calculated parameters need to be retried somehow and the user can save them as he wishes. Maybe with an lv_event_cb_t argument in touchscreen_cal_create that is called when the calibration is ready.

The result could be a Calibration matrix which can be applied on the raw coordinates by an additional function like lv_tpcal_apply_matrix(&x, &y, matrix).

What do you think?

Thanks for your response. I agree that non-volatile calibration storage is better left out of LVGL - but one or more examples would be useful.

Regarding postponed coordinate calculation, I have layout disabled, see https://github.com/cjheath/lvgl_touchscreen_cal/blob/2750a50bd97556d2880d40c4317c921765dc60b6/lvgl_touchscreen_cal.c#L66 (BTW there is no documented way to do this - but it looks like it should work?)

With layout disabled, I expect lv_obj_get_x to return exactly what the preceding lv_obj_set_x applied. Ahh - perhaps I should have done that on the screen, not on the button. I’ll try that.

This line also is problematic. I had tried to set the position of the instruction text by calculating its size. None of the methods I tried worked, and the position values always seemed to put the text at the bottom right. So I removed it and let it default to centre, which works well enough: lvgl_touchscreen_cal/lvgl_touchscreen_cal.c at 2750a50bd97556d2880d40c4317c921765dc60b6 · cjheath/lvgl_touchscreen_cal · GitHub

Before we can consider the correct output matrix, we need access to the untransformed input. I’m using the XPT2046 driver, which uses configuration constants. I need to read that code to see if it can even accept a configuration at run-time, but regardless of that, there should be a generic interface for pointing devices that allows fetching the untransformed values, so that a new transformation can be applied. Does this already exist but I’ve missed it?

In general the transformation from input hardware coordinates to screen coordinates needs generic support.

An example of the get/set problem is in these two lines. current_x/y has been set on entry to the function, and on the first call, the target had been set to the centre of the screen (code removed). But the get did not return the correct values (0 instead) which was also the animation end point, so my code below was skipped.

I had to add these two lines to make it work correctly.

Regarding the text sizing, I used lv_obj_update_layout(obj) but could still not get the size of the text. I’m not sure if that was my mistake somewhere.

Unfortunately, it’s a little bit more complicated. Even the grandparent’s layout can affect the position and the size of the object. E.g. if the grandparent is in a grid and it’s stretched all children move with it.

It seems working for me. Can you send a short code snippet to reproduce the issue?

No generic method for this at this moment. Let’s make some experiments with this using your new tpcal app and if all works well we can add a matrix (or so) parameter to lv_indev_drv that is applied automatically by LVGL.

Firstly, I forgot to thank you for this amazing library. I spent the most productive decade (the 90’s) of my career writing a commercial product that bridged all major GUI widget toolkits, but LVGL stands out as a brilliant piece of work. I will have more suggestions about some possible future directions for it, but later.

I think you misunderstand. I don’t need the layout or screen geometry of this object. I only want to retrieve the value of the x or y attribute that was just set. If the code cannot return the same value that was just set, it needs to be fixed so it can. Attributes I can set need to be stable, isolated from attributes that are dynamically re-computed.

I will try to isolate it for you, but the repositioning needs to be done after this. Fetching the text size didn’t work, even after a layout update:

I will have a look at the code and see if I can see a sensible way to implement this.

Happy to hear that! ^^
I’m curious to hear your ideas.

The most obvious solution would be to update layouts in lv_obj_get_x/y/width/etc functions but it needs to be examined carefully for performance drop.