天天看點

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校準程式。

繼續閱讀