Введение

Совсем недавно, проводя ревизию в одной из старых лабораторий университета, руководство поставило задачу как‑то использовать часть её старого оснащения. В качестве пациента и героя дальнейшего рассказа будет выступать вот такое незамысловатое сооружение, которое когда‑то было одним из тренажеров для студентов‑движенцев — дежурных по станции.

Работало это хозяйство под управлением ПК начала «нулевых» с горячим сердцем в виде Pentium 4, с ОС Windows 2000 на борту. Речи о том, чтобы использовать эти старые компьютеры, разумеется не шло — времена уже не те, современный софт и средства разработки далеко переросли и характеристики этого процессора и этой ОС. Системные блоки, естественно, отправятся в утиль.

Единственным, что можно хоть как‑то использовать оказались телевизор, то же не слишком изящный по нынешним временам — NEC LCD4010 c разрешением 1360×768, и два сенсорных встраиваемых монитора Elo Touchscreen с разрешением 1024×768. Было решено подключить это хозяйство к более‑менее современному компьютеру с Linux на борту и попробовать собрать на этом уже другой тренажер, для локомотивщиков.

Тут надо отметить, что старую технику я люблю, но вот использовать её в рабочих решениях, по понятным причинам как‑то не очень. Если с телевизором все более‑менее понятно, хоть и габаритный и тяжелый, с VGA‑интерфейсом, но завести его через кабель DisplayPort‑VGA в общем‑то задача тривиальная. Другое дело — Elo Touchscreen, как выяснилось модели ET1547L-8SWC оптимизма явно не внушал.

Elo Touchscreen ET1547L-8SWC
Elo Touchscreen ET1547L-8SWC
Маркировка с обратной стороны монитора
Маркировка с обратной стороны монитора

Однако, настоящие джедаи так просто не сдаются, поэтому

1. Обследование пациента: анамнез

Видеоинтерфейс сенсорного экрана — VGA, что в общем не является проблемой. Разрешение 1024×768 — тоже проблемой не является, современные встраиваемые 15-дюймовые Elo тоже имеют такое разрешение, по крайней мере те, что закупались нами для тренажеров уже в 2017 году. Но современные мониторы имеют разъем DisplayPort, а для touch‑сенсора используется USB‑интерфейс.

Здесь же я увидел нечто, что сначала повергло меня в задумчивость.

Интерфейс touch-серсора Elo ET1547L-8SWC
Интерфейс touch-серсора Elo ET1547L-8SWC

Два загадочных зеленых разъема тянулись к плате, воткнутой в PCI‑слот. Кто‑то из читателей на этом моменте нетерпеливо воскликнет — «Это же COM‑порты, идиот!» и будет совершенно прав, но для меня, не работавшего с такими решениями в области сенсорных экранов этот ответ был неочевиден. Тем более что кабель ну никак не был похож на стандартный нуль‑модемный, с двумя «мамами».

Загадочный кабель с "мамой" и "папой" формата DB9
Загадочный кабель с "мамой" и "папой" формата DB9

На момент изучения вопроса у меня было даже подозрение что это USB, но в каком‑то проприетарном исполнении, с проприетарной же PCI‑платой сопряжения, драйверов на которую в 2023 году не найти. Плата была извлечена из корпуса старого системного блока

PCI-плата сопряжения
PCI-плата сопряжения
Маркировка чипов крупным планом
Маркировка чипов крупным планом

маркировка чипов была отдана на съедение гуглу и выяснилось что плата ничто иное, как PCI‑расширитель COM‑портов. Причем установка её в материнскую плату добавляла в систему три порта в формате DB9 и один в формате DB25.

Порты COM (RS232) формата DB9 и DB25 на основной плате
Порты COM (RS232) формата DB9 и DB25 на основной плате
Два порта формата DB9 на дополнительной планке
Два порта формата DB9 на дополнительной планке

Это было, как в известном анекдоте — хорошая и плохая новость одновременная. Хорошая в том, что такая плата наверняка поддерживается ядром Linux из коробки и не нужно будет искать драйвера. Плохая заключалась в моем опасении, что распайка странного кабеля «папа»‑»мама» будет отличатся от стандартной, а может там и ещё какой сюрприз — кто их знает, эти корпорации...

Подключение нуль‑модемного кабеля к двум портам на плате показало — да, Linux прекрасно работает с данной платой, второе — распайка как портов так и странного зеленого кабеля совершенно стандартная. Rx встретился с Tx, судя по весело побежавшим сообщениям терминала, отправляемым из одного порта в другой.

Соответственно, вместо старой PCI‑платы расширителя, можно, в современном системнике, использовать и новую, на интерфейсе PCI‑Express. Таким образом первая часть Марлезонского балета завершилась и увенчалась, для порядка, написанием правила udev для назначения прав доступа на COM‑порты

/etc/udev/rules.d/90-serial.rules

SUBSYSTEM=="tty",ACTION=="add",KERNEL=="ttyS[0-9]",MODE="0666"

Сборка тестовой системы, однако, показала, что тактильный сенсор на экранах не подает никаких признаков жизни. Я‑то привык к тому что современные мониторы Elo, крайний из которых я использовал в 2021 году для тренажера электровоза ЧС2т, заводятся в Linux из коробки, и даже, часто не нуждаются ни в установке софта от производителя, ни даже в калибровке, если таковой монитор один. Здесь же предстояло ещё много вкусного красноглазия. Но наживка мной была захвачена, запустить этот экран в работу стало делом принципа.

2. Вторая часть Марлезонского балета: поиски во теме

Разумеется, на технику начало 2000-х на официальном сайте Elo Touchsystems ничего не нашлось. При том, что мой опыт работы с изделиями этой фирмы достаточно положителен и я доволен — мало по мало они упорядочили поддержку Linux для всех своих актуальных устройств.

Мой коллега, работавший над проблемой вместе со мной, под моё недовольное брюзжание о том что найденное в сети для ядер Linux версии 2.4 — 2.6 работать не будет в принципе, проведя масштабный поиск наткнулся таки на архив с софтом под Elo Serial Touchscreen версии 3.6.0 от 2021 года, где нашлись и модуль ядра и программы калибровки, совместимые с ядрами Linux версии 5.x.

В дистрибутиве Arch Linux, который использован на тестовой машине, на момент написания данной статьи присутствует ядро версии 6.2.9. Но, когда скачанный софт, согласно инструкции был развернут на целевой системе, и согласно инструкции же, загружен оригинальные модуль ядра от Elo Touchsystems, таки да — курсор, хоть и невпопад, но забегал по экрану. Значит сенсор жив и работает, но экраны не откалиброваны просто.

Утилита калибровки в архиве с софтом Elo нашлась. И вот тут‑то и началась настоящая свистопляска длиной в неделю.

Не хотели эти экраны калиброваться! Что мы не делали — по разному настраивали xorg.conf то объединяя три экрана в один, то разнося каждый монитор на свой экран, пробовали и те настройки утилит калибровки (коих в комплекте оказалось несколько) то эти. Устойчиво работала только конфигурация с одним единственным сенсорным экраном. Стоило подключить второй экран или телевизор — все тут же шло прахом. Дело усугублялось тем, что весь сервисный софт Elo (кроме модуля ядра) — проприетарный, исходники не посмотреть, причину не понять.

В голову закралась мысль, что современные мониторы Elo работают в Linux «из коробки», не требуя применения специализированного софта для своей настройки в большинстве случаев. Значит, наверняка, решение существует среди открытого софта. И эта мысль оказалась верной — первое же, достаточно яростное, гугление вывело нас... в репозторий ядра Linux где оказался вот такой код

linux/drivers/input/touchscreen/elo.c
// SPDX-License-Identifier: GPL-2.0-only
/*
 * Elo serial touchscreen driver
 *
 * Copyright (c) 2004 Vojtech Pavlik
 */


/*
 * This driver can handle serial Elo touchscreens using either the Elo standard
 * 'E271-2210' 10-byte protocol, Elo legacy 'E281A-4002' 6-byte protocol, Elo
 * legacy 'E271-140' 4-byte protocol and Elo legacy 'E261-280' 3-byte protocol.
 */

#include <linux/errno.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/input.h>
#include <linux/serio.h>
#include <linux/ctype.h>

#define DRIVER_DESC	"Elo serial touchscreen driver"

MODULE_AUTHOR("Vojtech Pavlik <vojtech@ucw.cz>");
MODULE_DESCRIPTION(DRIVER_DESC);
MODULE_LICENSE("GPL");

/*
 * Definitions & global arrays.
 */

#define ELO_MAX_LENGTH		10

#define ELO10_PACKET_LEN	8
#define ELO10_TOUCH		0x03
#define ELO10_PRESSURE		0x80

#define ELO10_LEAD_BYTE		'U'

#define ELO10_ID_CMD		'i'

#define ELO10_TOUCH_PACKET	'T'
#define ELO10_ACK_PACKET	'A'
#define ELI10_ID_PACKET		'I'

/*
 * Per-touchscreen data.
 */

struct elo {
	struct input_dev *dev;
	struct serio *serio;
	struct mutex cmd_mutex;
	struct completion cmd_done;
	int id;
	int idx;
	unsigned char expected_packet;
	unsigned char csum;
	unsigned char data[ELO_MAX_LENGTH];
	unsigned char response[ELO10_PACKET_LEN];
	char phys[32];
};

static void elo_process_data_10(struct elo *elo, unsigned char data)
{
	struct input_dev *dev = elo->dev;

	elo->data[elo->idx] = data;

	switch (elo->idx++) {
	case 0:
		elo->csum = 0xaa;
		if (data != ELO10_LEAD_BYTE) {
			dev_dbg(&elo->serio->dev,
				"unsynchronized data: 0x%02x\n", data);
			elo->idx = 0;
		}
		break;

	case 9:
		elo->idx = 0;
		if (data != elo->csum) {
			dev_dbg(&elo->serio->dev,
				"bad checksum: 0x%02x, expected 0x%02x\n",
				 data, elo->csum);
			break;
		}
		if (elo->data[1] != elo->expected_packet) {
			if (elo->data[1] != ELO10_TOUCH_PACKET)
				dev_dbg(&elo->serio->dev,
					"unexpected packet: 0x%02x\n",
					 elo->data[1]);
			break;
		}
		if (likely(elo->data[1] == ELO10_TOUCH_PACKET)) {
			input_report_abs(dev, ABS_X, (elo->data[4] << 8) | elo->data[3]);
			input_report_abs(dev, ABS_Y, (elo->data[6] << 8) | elo->data[5]);
			if (elo->data[2] & ELO10_PRESSURE)
				input_report_abs(dev, ABS_PRESSURE,
						(elo->data[8] << 8) | elo->data[7]);
			input_report_key(dev, BTN_TOUCH, elo->data[2] & ELO10_TOUCH);
			input_sync(dev);
		} else if (elo->data[1] == ELO10_ACK_PACKET) {
			if (elo->data[2] == '0')
				elo->expected_packet = ELO10_TOUCH_PACKET;
			complete(&elo->cmd_done);
		} else {
			memcpy(elo->response, &elo->data[1], ELO10_PACKET_LEN);
			elo->expected_packet = ELO10_ACK_PACKET;
		}
		break;
	}
	elo->csum += data;
}

static void elo_process_data_6(struct elo *elo, unsigned char data)
{
	struct input_dev *dev = elo->dev;

	elo->data[elo->idx] = data;

	switch (elo->idx++) {

	case 0:
		if ((data & 0xc0) != 0xc0)
			elo->idx = 0;
		break;

	case 1:
		if ((data & 0xc0) != 0x80)
			elo->idx = 0;
		break;

	case 2:
		if ((data & 0xc0) != 0x40)
			elo->idx = 0;
		break;

	case 3:
		if (data & 0xc0) {
			elo->idx = 0;
			break;
		}

		input_report_abs(dev, ABS_X, ((elo->data[0] & 0x3f) << 6) | (elo->data[1] & 0x3f));
		input_report_abs(dev, ABS_Y, ((elo->data[2] & 0x3f) << 6) | (elo->data[3] & 0x3f));

		if (elo->id == 2) {
			input_report_key(dev, BTN_TOUCH, 1);
			input_sync(dev);
			elo->idx = 0;
		}

		break;

	case 4:
		if (data) {
			input_sync(dev);
			elo->idx = 0;
		}
		break;

	case 5:
		if ((data & 0xf0) == 0) {
			input_report_abs(dev, ABS_PRESSURE, elo->data[5]);
			input_report_key(dev, BTN_TOUCH, !!elo->data[5]);
		}
		input_sync(dev);
		elo->idx = 0;
		break;
	}
}

static void elo_process_data_3(struct elo *elo, unsigned char data)
{
	struct input_dev *dev = elo->dev;

	elo->data[elo->idx] = data;

	switch (elo->idx++) {

	case 0:
		if ((data & 0x7f) != 0x01)
			elo->idx = 0;
		break;
	case 2:
		input_report_key(dev, BTN_TOUCH, !(elo->data[1] & 0x80));
		input_report_abs(dev, ABS_X, elo->data[1]);
		input_report_abs(dev, ABS_Y, elo->data[2]);
		input_sync(dev);
		elo->idx = 0;
		break;
	}
}

static irqreturn_t elo_interrupt(struct serio *serio,
		unsigned char data, unsigned int flags)
{
	struct elo *elo = serio_get_drvdata(serio);

	switch (elo->id) {
	case 0:
		elo_process_data_10(elo, data);
		break;

	case 1:
	case 2:
		elo_process_data_6(elo, data);
		break;

	case 3:
		elo_process_data_3(elo, data);
		break;
	}

	return IRQ_HANDLED;
}

static int elo_command_10(struct elo *elo, unsigned char *packet)
{
	int rc = -1;
	int i;
	unsigned char csum = 0xaa + ELO10_LEAD_BYTE;

	mutex_lock(&elo->cmd_mutex);

	serio_pause_rx(elo->serio);
	elo->expected_packet = toupper(packet[0]);
	init_completion(&elo->cmd_done);
	serio_continue_rx(elo->serio);

	if (serio_write(elo->serio, ELO10_LEAD_BYTE))
		goto out;

	for (i = 0; i < ELO10_PACKET_LEN; i++) {
		csum += packet[i];
		if (serio_write(elo->serio, packet[i]))
			goto out;
	}

	if (serio_write(elo->serio, csum))
		goto out;

	wait_for_completion_timeout(&elo->cmd_done, HZ);

	if (elo->expected_packet == ELO10_TOUCH_PACKET) {
		/* We are back in reporting mode, the command was ACKed */
		memcpy(packet, elo->response, ELO10_PACKET_LEN);
		rc = 0;
	}

 out:
	mutex_unlock(&elo->cmd_mutex);
	return rc;
}

static int elo_setup_10(struct elo *elo)
{
	static const char *elo_types[] = { "Accu", "Dura", "Intelli", "Carroll" };
	struct input_dev *dev = elo->dev;
	unsigned char packet[ELO10_PACKET_LEN] = { ELO10_ID_CMD };

	if (elo_command_10(elo, packet))
		return -1;

	dev->id.version = (packet[5] << 8) | packet[4];

	input_set_abs_params(dev, ABS_X, 96, 4000, 0, 0);
	input_set_abs_params(dev, ABS_Y, 96, 4000, 0, 0);
	if (packet[3] & ELO10_PRESSURE)
		input_set_abs_params(dev, ABS_PRESSURE, 0, 255, 0, 0);

	dev_info(&elo->serio->dev,
		 "%sTouch touchscreen, fw: %02x.%02x, features: 0x%02x, controller: 0x%02x\n",
		 elo_types[(packet[1] -'0') & 0x03],
		 packet[5], packet[4], packet[3], packet[7]);

	return 0;
}

/*
 * elo_disconnect() is the opposite of elo_connect()
 */

static void elo_disconnect(struct serio *serio)
{
	struct elo *elo = serio_get_drvdata(serio);

	input_get_device(elo->dev);
	input_unregister_device(elo->dev);
	serio_close(serio);
	serio_set_drvdata(serio, NULL);
	input_put_device(elo->dev);
	kfree(elo);
}

/*
 * elo_connect() is the routine that is called when someone adds a
 * new serio device that supports Gunze protocol and registers it as
 * an input device.
 */

static int elo_connect(struct serio *serio, struct serio_driver *drv)
{
	struct elo *elo;
	struct input_dev *input_dev;
	int err;

	elo = kzalloc(sizeof(struct elo), GFP_KERNEL);
	input_dev = input_allocate_device();
	if (!elo || !input_dev) {
		err = -ENOMEM;
		goto fail1;
	}

	elo->serio = serio;
	elo->id = serio->id.id;
	elo->dev = input_dev;
	elo->expected_packet = ELO10_TOUCH_PACKET;
	mutex_init(&elo->cmd_mutex);
	init_completion(&elo->cmd_done);
	snprintf(elo->phys, sizeof(elo->phys), "%s/input0", serio->phys);

	input_dev->name = "Elo Serial TouchScreen";
	input_dev->phys = elo->phys;
	input_dev->id.bustype = BUS_RS232;
	input_dev->id.vendor = SERIO_ELO;
	input_dev->id.product = elo->id;
	input_dev->id.version = 0x0100;
	input_dev->dev.parent = &serio->dev;

	input_dev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_ABS);
	input_dev->keybit[BIT_WORD(BTN_TOUCH)] = BIT_MASK(BTN_TOUCH);

	serio_set_drvdata(serio, elo);
	err = serio_open(serio, drv);
	if (err)
		goto fail2;

	switch (elo->id) {

	case 0: /* 10-byte protocol */
		if (elo_setup_10(elo)) {
			err = -EIO;
			goto fail3;
		}

		break;

	case 1: /* 6-byte protocol */
		input_set_abs_params(input_dev, ABS_PRESSURE, 0, 15, 0, 0);
		fallthrough;

	case 2: /* 4-byte protocol */
		input_set_abs_params(input_dev, ABS_X, 96, 4000, 0, 0);
		input_set_abs_params(input_dev, ABS_Y, 96, 4000, 0, 0);
		break;

	case 3: /* 3-byte protocol */
		input_set_abs_params(input_dev, ABS_X, 0, 255, 0, 0);
		input_set_abs_params(input_dev, ABS_Y, 0, 255, 0, 0);
		break;
	}

	err = input_register_device(elo->dev);
	if (err)
		goto fail3;

	return 0;

 fail3: serio_close(serio);
 fail2:	serio_set_drvdata(serio, NULL);
 fail1:	input_free_device(input_dev);
	kfree(elo);
	return err;
}

/*
 * The serio driver structure.
 */

static const struct serio_device_id elo_serio_ids[] = {
	{
		.type	= SERIO_RS232,
		.proto	= SERIO_ELO,
		.id	= SERIO_ANY,
		.extra	= SERIO_ANY,
	},
	{ 0 }
};

MODULE_DEVICE_TABLE(serio, elo_serio_ids);

static struct serio_driver elo_drv = {
	.driver		= {
		.name	= "elo",
	},
	.description	= DRIVER_DESC,
	.id_table	= elo_serio_ids,
	.interrupt	= elo_interrupt,
	.connect	= elo_connect,
	.disconnect	= elo_disconnect,
};

module_serio_driver(elo_drv);

Исходя из комментария в начале кода, понятно, что модуль ядра для Elo и именно с последовательным портом в качестве интерфейса присутствует в ядре Linux, а может быть только начал писаться — но аж в 2004 году! А крайний коммит датирован 2021 годом, то есть драйвер во вполне актуальном состоянии. Выполнение

# modprobe elo

и последующее

# lsmod | grep elo

дало следующий выхлоп

Стандартный модуль ядра для Elo Serial Touchscreen загружен
Стандартный модуль ядра для Elo Serial Touchscreen загружен

Оставалось только понять, как заставить загруженный драйвер правильно взаимодействовать с Xorg.

Перед началом новой фазы экспериментов было решено откатиться на такую конфигурацию мониторов

Три монитора как один виртуальный экран (Screen0). Крайние мониторы - сенсорные Elo
Три монитора как один виртуальный экран (Screen0). Крайние мониторы - сенсорные Elo

3. Медитация с xinput, или третья часть Марлезонского балета

Не буду перечислять бесчисленные Stack Owerflow, и прочие ресурсы, которые пришлось перешерстить в поисках решения. А оно, что удивительно, нашлось на... ЛОРе, вот в этом трэде. Исходя из полученной информации в системе были установлены пакеты

# pacman -S xorg-xinput linuxconsole

Прежде всего, следовало понять, как связать экран с соответствующим ему COM‑портом. Эта задача решилась просто

# inputattach -elo /dev/ttyS6 --daemon
# inputattach -elo /dev/ttyS7 --daemon

Ключик ‑elo как раз и сообщает утилите inputattach какой модуль ядра загрузить. После выполнения первой из команд, если модуль elo и не был загружен, то он загрузится. Далее, в качестве параметра указывается COM‑порт, при этом, используя модуль ядра, inputattach находит на данном порту сенсорный экран и инициализирует его. Ключ ‑daemon запускает экземпляр процесса inputattach в фоне, отпуская консоль.

После этой процедуры курсор опять забегал по экрану, при прикосновению к нему, но опять забегал как умалишенный. Применение ветхой утилиты xinput_calibrator ничего толком не дало — генерируемые им параметры Xorg адекватно не воспринимал. Однако первый этап — заставить систему загрузить модуль ядра и инициализировать экран — прошел успешно, о чем свидетельствует вот такой выхлоп

[loco@archlinux ~]$ xinput
⎡ Virtual core pointer                    	    id=2	[master pointer  (3)]
⎜   ↳ Virtual core XTEST pointer            	id=4	[slave  pointer  (2)]
⎜   ↳ Elo Serial Touchscreen             	    id=6	[slave  pointer  (2)]
⎜   ↳ Elo Serial Touchscreen          	        id=7	[slave  pointer  (2)]
⎜   ↳ Logitech USB-PS/2 Optical Mouse           id=10	[slave  pointer  (2)]
⎣ Virtual core keyboard                   	    id=3	[master keyboard (2)]
    ↳ Virtual core XTEST keyboard             	id=5	[slave  keyboard (3)]
    ↳ Video Bus                               	id=6	[slave  keyboard (3)]
    ↳ Power Button                            	id=7	[slave  keyboard (3)]

Наши сенсорные экраны в списке с идентификаторами id=6 и id=7. Посмотрим поближе на свойства этих устройств

[loco@archlinux ~]$ xinput list-pros 6
Device 'Elo Serial Touchscreen':
	Device Enabled (153):	1
	Coordinate Transformation Matrix (181):	1.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 1.000000
	libinput Rotation Angle (276):	0.000000
	libinput Rotation Angle (277):	0.000000
    libinput Calibration Matrix (278):	1.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 1.000000
	libinput Calibration Matrix Default (279):	1.000000, 0.000000, 0.000000, 0.000000, 1.000000, 0.000000, 0.000000, 0.000000, 1.000000
    libinput Send Events Modes Available (280):	1, 0
	libinput Send Events Mode Available (281):	0, 0
    libinput Send Events Mode Available Default (280):	0, 0
    Device Node (283):       "/dev/input/event16"
    Device Product ID (284):         41, 0

Добытая информация и собственные размышления привели к выводу, что стоило бы обратить внимание на параметр с номером (278) libinput Calibration Matrix, которая в неоткалиброванном экране оказалась диагональной единичной матрицей 3х3. Очевидно, её модификация для каждого из экранов должна была бы привести нас к нужному результату, и было принято отчаянное решение - выполнить закат Солнца вручную, настроив эту калибровочную матрицу самостоятельно. Формулы, приведенные в частности по этой ссылке, эффекта не дали. Откуда они вообще взялись и как были выведены - тот еще вопрос. Поэтому пришлось применить оружие настоящего джедая, ещё более изящное чем световой меч, а именно - аппарат линейной алгебры.

4. Немного математики

Для начала был проведен эксперимент, имевший своей целью выяснить куда попадает курсор при касание угловых точек экрана. Этот эксперимент дал, в существующей конфигурации результат, выражаемый рисунком

Тестирование некалиброванного экрана
Тестирование некалиброванного экрана

Черными парами координат без скобок показаны фактические координаты восьми точек, куда прикасался палец в процессе эксперимента, выраженные в долях ширины и высоты обще области ввода, с учетом разрешения всех задействованных мониторов. Зeлеными парами в скобках показаны фактические координаты точек, куда Xorg перемещал курсор. Очевидно, что здесь мы имеем дело с отображением одного линейного пространства в другое, при этом, такое преобразование мы можем представить в виде

\vec{r}_v = M_{sv} \, C \, \vec{r}_s

или в развернутом виде

\left[\begin{matrix} x_v \\ y_v \\ 1 \end{matrix}\right] = \left[ \begin{matrix} a& 0& c \\ 0& e& f \\ 0& 0& 1\end{matrix} \right] \left[ \begin{matrix} c_{11}& c_{12}& c_{13} \\ c_{21}& c_{22}& c_{23} \\ 0 & 0& 1\end{matrix} \right]\left[\begin{matrix} x_s \\ y_s \\ 1 \end{matrix}\right]

Очевидно, что при некалиброванном экране матрица C — единичная, никак не влияет на результат преобразования, а матрица Msv — то правило, по которому ведет преобразование некалиброванный экран. Очевидно, для совпадения координат точки, куда попадает палец xs и ys и туда, куда в итоге перемещается курсор xv и yv необходимо подобрать такую матрицу C, чтобы оказалось что

M_{sv} \, C = E

где E — единичная матрица. Очевидно, что для этого надо обратить матрицу Msv

C = M_{sv}^{-1}

При единичной калибровочной матрице, получаем два уравнения

x_v = a \, x_s + c \\ y_v = e \, y_s + f

Для левого экрана, взяв точки 1 и 3 (диагональ) получаем конкретные уравнения

1 = c \\ 0 = f \\ 0 = 0.3 \ a + c \\ 1 = e

откуда вытекает, что матрица Msv (которая соответствует не калиброванному экрану) равна

M_{sv}^{left} = \left[ \begin{matrix} -3.33& 0.00& 1.00 \\ 0.00& 1.00& 0.00 \\ 0.00 & 0.00& 1.00\end{matrix} \right]

обращая которую получаем калибровочную матрицу для левого экрана

С^{left} = \left[ \begin{matrix} -0.30& 0.00& 0.30 \\ 0.00& 1.00& 0.00 \\ 0.00 & 0.00& 1.00\end{matrix} \right]

Аналогично, для правого экрана, имеем уравнения

1 = 0.7 \,a + c \\0 = f\\0 = a + c \\1 = e

получая матрицу текущего преобразования

M_{sv}^{right} = \left[ \begin{matrix} -3.33& 0.00& 3.33 \\ 0.00& 1.00& 0.00 \\ 0.00 & 0.00& 1.00\end{matrix} \right]

и калибровочную матрицу

С^{right} = \left[ \begin{matrix} -0.30& 0.00& 1.00 \\ 0.00& 1.00& 0.00 \\ 0.00 & 0.00& 1.00\end{matrix} \right]

Остается только задать эти матрицы для каждого из экранов, использую id, выводимые xinput

[loco@archlinux]$ xinput 6 set-prop "libinput Calibration Matrix" -0.3 0.0 0.3 0.0 1.0 0.0 0.0 0.0 1.0
[loco@archlinux]$ xinput 6 set-prop "libinput Calibration Matrix" -0.3 0.0 0.3 0.0 1.0 0.0 0.0 0.0 1.0

ииии... дело в общем-то в шляпе!

5. Финальная настройка

Все эти настройки после перезагрузки, разумеется превратятся в «тыкву». Чтобы этого не произошло, следует, во первых, при загрузке системы и инициализации графики выполнить запуск демонов inputattach, что решаем средствами systemd, написав такой юнит

/etc/systemd/system/elotouch@.service

[Unit]
Description=inputattach for Elo Serial Touchscreen

[Service]
Type=forking
ExecStart=/usr/bin/inputattach -elo /dev/%I --daemon
Restart=always

[Install]
WantedBy=graphical.target

после чего, протестировав службу, помещаем её в автозагрузку, передав в качестве параметра имена COM‑портов, на которых висят сенсоры.

# systemctl enable elotouch@ttyS6
# systemctl enable elotouch@ttyS7

Для загрузки матрицы калибровки, настроим в xorg.conf соответствующие устройства ввода

/etc/X11/xorg.conf
# nvidia-settings: X configuration file generated by nvidia-settings
# nvidia-settings:  version 530.41.03

Section "ServerLayout"
    Identifier     "Layout0"
    Screen      0  "Screen0" 0 0
    InputDevice    "Keyboard0" "CoreKeyboard"
    InputDevice    "Mouse0" "CorePointer"
	InputDevice	   "elotouch0" "SendCoreEvents"
	InputDevice	   "elotouch1" "SendCoreEvents"
    Option         "Xinerama" "0"
EndSection

Section "Files"
EndSection

Section "InputDevice"
    # generated from default
    Identifier     "Mouse0"
    Driver         "mouse"
    Option         "Protocol" "auto"
    Option         "Device" "/dev/psaux"
    Option         "Emulate3Buttons" "no"
    Option         "ZAxisMapping" "4 5"
EndSection

Section "InputDevice"
    # generated from default
    Identifier     "Keyboard0"
    Driver         "kbd"
EndSection

Section "InputDevice"
        Identifier      "elotouch0"
        Driver	"elo"
	Option "Device" "/dev/input/event16"
	Option "CalibrationMatrix" "-0.3 0.0 0.3 0.0 1.0 0.0 0.0 0.0 1.0"
EndSection

Section "InputDevice"
        Identifier      "elotouch1"
        Driver	"elo"
	Option "Device" "/dev/input/event15"
	Option "CalibrationMatrix" "-0.3 0.0 1.0 0.0 1.0 0.0 0.0 0.0 1.0"
EndSection

Section "Monitor"
    # HorizSync source: edid, VertRefresh source: edid
    Identifier     "Monitor0"
    VendorName     "Unknown"
    ModelName      "NEC LCD4010"
    HorizSync       31.0 - 92.0
    VertRefresh     48.0 - 85.0
    Option         "DPMS"
EndSection

Section "Device"
    Identifier     "Device0"
    Driver         "nvidia"
    VendorName     "NVIDIA Corporation"
    BoardName      "NVIDIA GeForce GTX 1060 3GB"
EndSection

Section "Screen"
    Identifier     "Screen0"
    Device         "Device0"
    Monitor        "Monitor0"
    DefaultDepth    24
    Option         "Stereo" "0"
    Option         "nvidiaXineramaInfoOrder" "DFP-0"
    Option         "metamodes" "DVI-D-0: nvidia-auto-select +1024+0, HDMI-0: nvidia-auto-select +0+0, DP-4: nvidia-auto-select +2384+0"
    Option         "SLI" "Off"
    Option         "MultiGPU" "Off"
    Option         "BaseMosaic" "off"
    SubSection     "Display"
        Depth       24
    EndSubSection
EndSection

Внимание следует обратить на следующие секции

/etc/X11/xorg.conf

Section "InputDevice"
        Identifier      "elotouch0"
        Driver	"elo"
	Option "Device" "/dev/input/event16"
	Option "CalibrationMatrix" "-0.3 0.0 0.3 0.0 1.0 0.0 0.0 0.0 1.0"
EndSection

Section "InputDevice"
        Identifier      "elotouch1"
        Driver	"elo"
	Option "Device" "/dev/input/event15"
	Option "CalibrationMatrix" "-0.3 0.0 1.0 0.0 1.0 0.0 0.0 0.0 1.0"
EndSection

Здесь связываются между собой драйвер устройства, его нода в /dev/input, которую можно узнать из выхлопа xinput list-props. Кроме того, эти устройства следует указать в секции ServerLayout

/etc/X11/xorg.conf

Section "ServerLayout"
    Identifier     "Layout0"
    Screen      0  "Screen0" 0 0
    InputDevice    "Keyboard0" "CoreKeyboard"
    InputDevice    "Mouse0" "CorePointer"
	InputDevice	   "elotouch0" "SendCoreEvents"
	InputDevice	   "elotouch1" "SendCoreEvents"
    Option         "Xinerama" "0"
EndSection

Теперь, после перезагрузки и запуска "иксов" наши экраны инициализируются и откалибруются.

Выводы

Естественно, тесты работы данных экранов показали и низкую отзывчивость сенсора, и его нелинейность, усугубляющуюся к краям экрана. Всё таки технике скоро стукнет второй десяток. Однако, они вполне пригодны к использованию в некритичных к этим недостатках применениях.

Второй момент — всё это было бы трудно осуществимо в ОС семейства Windows. На версию 7 драйвера Elo Serial Touchscreen еще худо бедно ставятся, а вот на 10 или 11 — не дождетесь.

Надеюсь было интересно, спасибо за внимание и до новых встреч!

Комментарии (1)


  1. aborouhin
    04.04.2023 01:11

    О, а раз Вы с продукцией Elo хорошо знакомы, не подскажете по другой ситуации (но тоже надо извратиться :) ?
    Есть в домашнем хозяйстве две уличные (!) вандалостойкие (!!) 32-дюймовые тач-панели этого производителя (в своё время полученные от дружественной конторы безвозмездно, т.е. даром, по принципу "забирай, оно у нас на складе пятый год лежит, место занимает"). Тач-интерфейс у них уже более современный, USB. Трудятся они у меня они телевизорами под фильмы/ютуб, управляются, соответственно, смарт-ТВ коробочками под Android. Драйвера под Win и Linux есть, а вот под Андроид не нашёл никаких решений. Не то, чтобы для Смарт-ТВ тач был так сильно нужен, но обидно, что он там есть, а не работает.
    Не сталкивались, есть хоть какой-то шанс их с Андроидом заставить работать? Можно, конечно, поменять те андроидные смарт-ТВ коробочки на какие-нибудь малинки с линуксом, но ради одного тача ещё тратиться не хочется.