天天看点

Qt tslib校准程序

在电阻式的触摸屏上开发嵌入式应用时,Qt+tslib的组合很常见,可谓经典组合。tslib库中提供了一些测试程序,比如ts_calibrate,ts_test,可以用来对触摸屏进行校准,测试。在Qt中也提供了一个例程,也是用来触屏校准的(http://doc.qt.io/qt-4.8/qt-qws-mousecalibration-calibration-cpp.html) ,Qt库中把触摸屏模拟成鼠标设备来实现,我试了这种方法,发现校准后在/etc/pointercal产生的数据不大对,和ts_calibrate程序产生的数据相差很大,校准后触屏无法正常使用,而且用鼠标点点点也能触发校准完成。由于我的项目应用中是鼠标和触屏混用的,这种校准方式不可取,于是就想到了利用tslib库中的方法,来实现Qt上的触摸屏校准应用。

首先分下下tslib库中的test/ts_calibrate.c程序,去除显示部分(显示校准点十字框),代码如下。

typedef struct {
	int x[5], xfb[5];
	int y[5], yfb[5];
	int a[7];
} calibration;


int perform_calibration(calibration *cal) {
	int j;
	float n, x, y, x2, y2, xy, z, zx, zy;
	float det, a, b, c, e, f, i;
	float scaling = 65536.0;
	...
}

static void get_sample (struct tsdev *ts, calibration *cal,
			int index, int x, int y, char *name)
{
	...
	put_cross(x, y, 2 | XORMODE);
	...
	getxy (ts, &cal->x [index], &cal->y [index]);
	cal->xfb [index] = x;
	cal->yfb [index] = y;
}

int main()
{
	struct tsdev *ts;	calibration cal;
	int cal_fd;			char cal_buffer[256];
	char *tsdevice = NULL;	char *calfile = NULL;

	if( (tsdevice = getenv("TSLIB_TSDEVICE")) != NULL ) {
		ts = ts_open(tsdevice,0);
	} else {
		...
	}

	if (ts_config(ts)) {
		perror("ts_config");
		exit(1);
	}

	get_sample (ts, &cal, 0, 50,        50,        "Top left");
	get_sample (ts, &cal, 1, xres - 50, 50,        "Top right");
	get_sample (ts, &cal, 2, xres - 50, yres - 50, "Bot right");
	get_sample (ts, &cal, 3, 50,        yres - 50, "Bot left");
	get_sample (ts, &cal, 4, xres / 2,  yres / 2,  "Center");

	if (perform_calibration (&cal)) {
	
		if ((calfile = getenv("TSLIB_CALIBFILE")) != NULL) {
			cal_fd = open (calfile, O_CREAT | O_RDWR);
		} else {
			cal_fd = open ("/etc/pointercal", O_CREAT | O_RDWR);
		}
		sprintf (cal_buffer,"%d %d %d %d %d %d %d",
			 cal.a[1], cal.a[2], cal.a[0],
			 cal.a[4], cal.a[5], cal.a[3], cal.a[6]);
		write (cal_fd, cal_buffer, strlen (cal_buffer) + 1);
		close (cal_fd);
	} else {
		printf("Calibration failed.\n");
	}
}
           

分析下ts_calibrate.c中程序的校准流程如下。

程序中,触摸屏设备TSLIB_TSDEVICE校准文件TSLIB_CALIBFILE都是从环境变量中获取的,在配置tslib库中应该指定。TSLIB_TSDEVICE一般是/dev/input/event0文件,这个跟触屏设备有关,可以用cat /proc/bus/input/devices命令查看确认,或者直接在终端用cat /dev/input/event0命令,然后点击触摸屏,会有数据打印出。TSLIB_TSCALIBFILE一般是/etc/pointercal文件,文件中记录了计算后的校准值。通过ts_open()、ts_config()函数打开和初始化触摸屏设备后,在get_sample()函数中通过put_cross()函数显示光标,然后通过getxy()函数等待用户点击触屏上的光标,获取触点数据,然后存在cal结构体中。获取完5个校准点(左上、右上、右下、左下、中间)数据后,通过perform_calibration()进行计算,把校准值写入到/etc/pointercal文件中。

我们把ts_calibrate.c的程序移植到Qt应用中,显示部分肯定不能用在tslib库中的了。tslib库中是c程序,显示都是直接操作framebuffer,而Qt作为跨平台的GUI库,显示是基本功能,直接把ts_calibrate.c中校准部分移植过来即可,这样代码改动量最小。

校准程序很简单,要注意的一点是,getxy()函数是异步的,会阻塞等待用户点击触摸屏上的校准点,因此,getsample()函数有必要放到线程中运行,与Qt GUI线程分离开来。在Qt中进行校准的流程与tslib库中完全一致,就是显示部分采用Qt的库,代码参考了Qt库中的qws/mousecalibration/calibration.cpp文件。这里进行简要分析下。

首先是调用方式,把校准流程封装到Calibration类中,调用时,两行代码即可。

Calibration cal;
cal.exec();
           

在实例化Calibration对象后,会弹出一个校准界面,覆盖原有程序界面,校准完成后,返回原有程序界面。这个流程跟弹出一个对话框,点击对话框后返回类似。事实上,Calibration就是继承自QDialog类的。首先分析下Calibration类的构造函数。

Calibration::Calibration()
{
    QRect desktop = QApplication::desktop()->geometry();
    desktop.moveTo(QPoint(0, 0));
    setGeometry(desktop);

    setFocusPolicy(Qt::StrongFocus);
    setFocus();
    setModal(true);

    int width = qt_screen->deviceWidth();
    int height = qt_screen->deviceHeight();
    int dx, dy;
    QPoint *points = data.screenPoints;
    dx = 50;
    dy = 50;
    points[0] = QPoint(dx, dy);
    points[1] = QPoint(width - dx, dy);
    points[2] = QPoint(width - dx, height - dy);
    points[3] = QPoint(dx, height - dy);
    points[4] = QPoint(width / 2, height / 2);

    pressCount = 0;
}
           

构造函数中,定义了一个Qt窗口(全屏),把它设置为模态的,然后定义了要校准显示的5个十字光标的位置。再来看下exec()函数。

int Calibration::exec()
{
    activateWindow();
    calibThread = new CalibThread;
    if(calibThread->Config()){
        connect(calibThread, SIGNAL(nextPoint()),
                this, SLOT(OnNextPoint()), Qt::QueuedConnection);
        calibThread->start();
        int ret = QDialog::exec();
        return ret;
    }else{
        delete calibThread;
        return 0;
    }
}
           

在exec()函数中,首先将构造函数中定义的窗口置顶显示(这里会进行一次窗口刷新),然后定义了校准线程,注意这里校准线程与Calibration对象有一个信号和槽的绑定,作用后面会介绍。最后定义了一个QDialog(无显示内容)对象,让其以模态的方式运行。

接下来看校准线程,校准线程与tslib库中的程序就很一致了。其构造函数和run()函数如下。

CalibThread::CalibThread()
{
}

bool CalibThread::Config()
{
    char *tsdevice = NULL;
    if( (tsdevice = getenv("TSLIB_TSDEVICE")) != NULL ) {
        ts = ts_open(tsdevice,0);
        Log_d(QString("getenv TSLIB_TSDEVICE: %1").arg(tsdevice));
    } else {
        Log_e(QString("getenv TSLIB_TSDEVICE NULL").arg(tsdevice));
        return false;
    }

    if (!ts) {
        Log_e("ts NULL");
        return false;
    }
    if (ts_config(ts)) {
        Log_e("ts_config error");
        return false;
    }

    xres = qt_screen->deviceWidth();
    yres = qt_screen->deviceHeight();
    Log_d(QString("ts_config ok, xres: %1, yres: %2").arg(xres).arg(yres));
    return true;
}

void CalibThread::run()
{
    get_sample (ts, &cal, 0, 50,        50,        (char*)"Top left");
    emit nextPoint();

    get_sample (ts, &cal, 1, xres - 50, 50,        (char*)"Top right");
    emit nextPoint();

    get_sample (ts, &cal, 2, xres - 50, yres - 50, (char*)"Bot right");
    emit nextPoint();

    get_sample (ts, &cal, 3, 50,        yres - 50, (char*)"Bot left");
    emit nextPoint();

    get_sample (ts, &cal, 4, xres / 2,  yres / 2,  (char*)"Center");
    emit nextPoint();
}
           

线程中阻塞运行get_sample()函数5次,每次获取完校准点数据后,发射nextPoint()信号,通知Calibration类进行界面刷新,在下一个校准点显示十字光标。Calibration类中的槽函数和刷新函数如下。

void Calibration::paintEvent(QPaintEvent*)
{
    QPainter p(this);
    p.fillRect(rect(), Qt::white);

    QPoint point = data.screenPoints[pressCount];
    // Map to logical coordinates in case the screen is transformed
    QSize screenSize(qt_screen->deviceWidth(), qt_screen->deviceHeight());
    point = qt_screen->mapFromDevice(point, screenSize);

    p.fillRect(point.x() - 6, point.y() - 1, 13, 3, Qt::black);
    p.fillRect(point.x() - 1, point.y() - 6, 3, 13, Qt::black);
}

void Calibration::accept()
{
    if(pressCount == 5){
        Log_d("Calibration accept success, pressCount: 5");
    }else{
        Log_d("Error, pressCount is not 5");
        return;
    }

    if (calibThread->Perform_calibration()) {
        calibThread->Calibration_write();
    } else {
        Log_e("perform_calibration failed.");
    }

    delete calibThread;

    QWSServer::instance()->closeMouse();
    QWSServer::instance()->openMouse();
    QDialog::accept();
}

void Calibration::OnNextPoint()
{
    if (++pressCount < 5)
        repaint();
    else
        accept();
}
           

在Calibration类槽函数OnNextPoint()中,首先判断校准完5个点没,没有,则刷新绘制下一个点(在paintEvent()函数中完成),校准完,则跳转到accept()函数中进行校准值计算,并写入校准文件。程序中get_sample()函数,perform_calibration()函数都与tslib库中的完全一致,相当于调用了tslib中原生的校准方法。写校准值函数的程序如下。

int CalibThread::Calibration_write()
{
    char *calfile = NULL;
    char cal_buffer[256];
    unsigned int len;
    QFile file;

    if ((calfile = getenv("TSLIB_CALIBFILE")) != NULL) {
        file.setFileName(calfile);
        if(!file.open(QIODevice::ReadWrite | QIODevice::Text)){
            Log_e("file.open error");
            return -1;
        }
    } else {
        Log_e("TSLIB_CALIBFILE NULL");
        return -2;
    }
    len = sprintf(cal_buffer,"%d %d %d %d %d %d %d %d %d ",
                  cal.a[1], cal.a[2], cal.a[0],
                  cal.a[4], cal.a[5], cal.a[3], cal.a[6],
                  xres, yres);

    Log_d("Calibration constants: " + cal_buffer);

    file.write(cal_buffer, len);
    file.close();
    return 0;
}
           

完整的程序下载链接Qt tslib校准程序。

继续阅读