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
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.