Пишем драйвер пользовательского окружения для uinput на Raspberry Pi
Ради одного из своих небольших проектов на Raspberry Pi 2 я приобрел емкостной сенсорный дисплей Waveshare с демократичной ценой, скромным разрешением и сомнительной поддержкой. В коробке с дисплеем лежала DVD-R DL, и по заявлениям продавца, там лежали образы систем на базе Raspbian. Прочитать их мне не удалось, поиск решений в интернете подсказал, что драйвер, который там лежал, был и так не самым лучшим решением (уже скомпилированное ядро без исходников).
В процессе поиска я наткнулся на проект одного парня из дружественного Китая. Благодаря нему я смог прийти к своему решению.
Дело в том, что компания предоставила только двоичный драйвер для своего дисплея, слинковав его с raspbian'овским ядром. Это хорошо, до тех пор, пока вы остаетесь на родном ядре и не хотите ничего менять и не вести серьезную embedded-разработку. Но как только вместо Debian'a вы перейдете на buildroot, смените компилятор, пересоберете свое ядро и так далее — у вас не останется никакого драйвера, совместимого с вашей новоиспеченной операционкой вообще.
В таких случаях выручает драйвер пользовательского окружения, который представляет собой программу, которая передает модулю ядра данные в понятном линуксу виде.
Если мы посмотрим в лог dmesg, то увидим интересные нам строчки:
[ 3.518144] usb 1-1.5: new full-speed USB device number 4 using dwc_otg
[ 3.606036] udevd[174]: starting version 175
[ 3.631476] usb 1-1.5: New USB device found, idVendor=0eef, idProduct=0005
[ 3.641195] usb 1-1.5: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[ 3.653540] usb 1-1.5: Product: By ZH851
[ 3.659956] usb 1-1.5: Manufacturer: RPI_TOUCH
[ 3.659967] usb 1-1.5: SerialNumber: \xffffffc2\xffffff84\xffffff84\xffffffc2\xffffffa0\xffffffa0B54711U335
[ 3.678577] hid-generic 0003:0EEF:0005.0001: hiddev0,hidraw0: USB HID v1.10 Device [RPI_TOUCH By ZH851] on usb-bcm2708_usb-1.5/input0
Господин derekhe с гитхаба указал на флаг сборки ядра (ядра у меня, к сожалению, не было):
CONFIG_USB_EGALAX_YZH=y
Это позволяет прийти к заключению о том, что устройство является (или имитирует) клоном сенсора eGalaxy, а Waveshare просто перебили USB VendorId:ProductId. Так или иначе ядро создает устройство типа hidraw, в которое сенсорный экран плюет данные по 25 байт.
i@raspberrypi ~ $ sudo xxd -c 25 /dev/hidraw0
0000000: aa01 01bf 00bc bb01 0056 0019 018c 00ef 00b4 02ce 01d7 0182 cc .........V...............
00000af: aa01 01bf 00bc bb01 0056 0019 018c 00ef 00b4 02ce 01d7 0182 cc .........V...............
...
00000fa: aa00 0000 0000 bb00 0000 0000 0000 0000 0000 0000 0000 0000 00 .........................
Потыкав пальцами в экран можно разобрать формат сообщения:
- a[0]: 0xAA — начало сообщения
- a[1]: 0x01 — нажатие есть, 0x00 — нажатия нет
- a[2, 3]: X1
- a[4, 5]: Y1
- a[6]: 0xBB — разделитель
- a[7]: флаговая переменная, где наличие i-того бита начиная с младшего отвечает на нажатие i-той точки
- a[8, 9]: Y2
- a[10, 11]: X2
- a[12, 13]: Y3
- a[14, 15]: X3
- a[16, 17]: Y4
- a[18, 19]: X4
- a[20, 21]: Y5
- a[22, 23]: X5
- a[24]: 0xCC — разделитель
Найденный драйвер может и быстрое решение, но мне не понравился по следующим причинам:
- Мне кажется писать драйверы на интерпретируемом языке в Embedded не лучшей идей
- Драйвер имитирует поведение мыши, и игнорирует мультитач
Было принято решение устроить себе челендж и написать свой драйвер для uinput на C с мультитачем и udev'ом.
Для ленивых: исходники сырой версии драйвера лежат тут.
Прежде всего нужно осознавать, что все, что втыкается в USB можно и выткнуть, а поэтому нужно учитывать горячее подключение к порту.
В линуксе есть замечательная библиотека libudev, которая позволяет произвести перечисление подключенных устройств и мониторить добавление/удаление устройств.
Один из недостатков нашего случая — драйвер hidraw не несет информации о VendorId:ProductId устройства USB, к которому он привязан. Поэтому нужно делать перечисление устройств по драйверу hidraw, а затем искать родительское USB-устройство с указанными нами идентификаторами.
Важно: если мы хотим проверить уже подключенные устройства и следить за добавлением/удалением, необходимо выполнить действия в следующем порядке (и никак иначе):
- Инициализировать udev_monitor
- Запросить перечисление подключенных устройств udev_enumerate_get_list_entry
- Запустить цикл опроса udev_monitor
Получение списка устройств
С помощью функции udev_enumerate_add_match_subsystem и udev_monitor_filter_add_match_subsystem_devtype мы отсеиваем часть нерелевантных нам устройств. При получении указателя на нужный нам hidraw-девайс, нужно проверить, наш ли он:
int try_init_device(struct udev_device *dev) {
struct udev_device *devusb = udev_device_get_parent_with_subsystem_devtype(dev, "usb", "usb_device");
if (devusb) {
if (strcmp(udev_device_get_sysattr_value(devusb, "idVendor"), DEVICE_ID_VENDOR) == 0 &&
strcmp(udev_device_get_sysattr_value(devusb, "idProduct"), DEVICE_ID_PRODUCT) == 0)
{
return try_start_device_loop(udev_device_get_devnode(dev));
}
}
return -2;
}
Если условие выполнено, то мы запускаем в отдельном потоке цикл обработки событий от сенсорного экрана. Цикл считывает данные из /dev/hidraw* и записывает команды в выделенный ему /dev/input/eventX
В документации Linux описано два варианта реализации uinput драйвера для сенсорной панели:
Тип А:
ABS_MT_POSITION_X x[0]
ABS_MT_POSITION_Y y[0]
SYN_MT_REPORT
ABS_MT_POSITION_X x[1]
ABS_MT_POSITION_Y y[1]
SYN_MT_REPORT
SYN_REPORT
Тип B:
ABS_MT_SLOT 0
ABS_MT_TRACKING_ID 45
ABS_MT_POSITION_X x[0]
ABS_MT_POSITION_Y y[0]
ABS_MT_SLOT 1
ABS_MT_TRACKING_ID 46
ABS_MT_POSITION_X x[1]
ABS_MT_POSITION_Y y[1]
SYN_REPORT
Поскольку реализовывать второй протокол долго и алгоритмически сложно, будем реализовывать первый. Необходимо объявить характеристики устройства, такие как название, шина, идентификатор вендора и продукта, разрешение по осям и допустимые события для устройства.
struct uinput_user_dev user_dev;
memset(&user_dev, 0, sizeof(struct uinput_user_dev));
user_dev.absmin[ABS_MT_POSITION_X] = 0;
user_dev.absmax[ABS_MT_POSITION_X] = DEVICE_WIDTH;
user_dev.absmin[ABS_MT_POSITION_Y] = 0;
user_dev.absmax[ABS_MT_POSITION_Y] = DEVICE_HEIGHT;
user_dev.id.bustype = BUS_USB;
user_dev.id.vendor = DEVICE_ID_VENDOR_HEX;
user_dev.id.product = DEVICE_ID_PRODUCT_HEX;
user_dev.id.version = 1;
strcpy(user_dev.name, "Waveshare multitouch screen");
Поскольку мы работает с multitouch-устройством, то оси у нас соответственно ABS_MT_POSITION_X и ABS_MT_POSITION_Y. После открытия устройства мы заявляем о типах событий:
int abs_axes[] = {
ABS_MT_POSITION_X,
ABS_MT_POSITION_Y
};
int i;
for (i = 0; i < 2; ++i) {
if (suinput_enable_event(uinput_fd, EV_ABS, abs_axes[i]) == -1) {
close(uinput_fd);
entry.thread = 0;
pthread_exit(NULL);
return 0;
}
}
Читаем в цикле наш порт и в соответствии с разобранным выше протоколом создаем события для uinput. Для каждой нажатой точки требуется событие ABS_MT_POSITION_X, ABS_MT_POSITION_Y и SYN_MT_REPORT. Если нажатий больше нет, то передается SYN_MT_REPORT. В конце каждого пакета (набор точек или событие о том, что их больше нет) необходимо вызвать SYN_REPORT.
Драйвер зависит от libsuinput, pthreads, libudev и компилятора C99. Для сборки все это должно присутствовать в сборочном окружении:
gcc -std=c99 -Wall ./waveshare.c -pthread -lsuinput -ludev -o waveshare-touch-driver
Запускаем приложение от имени суперпользователя в фоне:
sudo waveshare-touch-driver &
И проверяем созданное устройство (к моей малине ни клавиатуры, ни мыши подключено не было, поэтому /dev/input/event0):
pi@raspberrypi ~ $ evtest /dev/input/event0
Input driver version is 1.0.1
Input device ID: bus 0x3 vendor 0xeef product 0x5 version 0x1
Input device name: "Waveshare multitouch screen"
Supported events:
Event type 0 (EV_SYN)
Event type 3 (EV_ABS)
Event code 53 (ABS_MT_POSITION_X)
Value 0
Min 0
Max 800
Event code 54 (ABS_MT_POSITION_Y)
Value 0
Min 0
Max 480
Properties:
Testing ... (interrupt to exit)
...
Event: time 20159.696497, type 3 (EV_ABS), code 53 (ABS_MT_POSITION_X), value 728
Event: time 20159.696497, type 3 (EV_ABS), code 54 (ABS_MT_POSITION_Y), value 41
Event: time 20159.696497, ++++++++++++++ SYN_MT_REPORT ++++++++++++
Event: time 20159.696497, type 3 (EV_ABS), code 53 (ABS_MT_POSITION_X), value 595
Event: time 20159.696497, type 3 (EV_ABS), code 54 (ABS_MT_POSITION_Y), value 154
Event: time 20159.696497, ++++++++++++++ SYN_MT_REPORT ++++++++++++
Event: time 20159.696497, type 3 (EV_ABS), code 53 (ABS_MT_POSITION_X), value 456
Event: time 20159.696497, type 3 (EV_ABS), code 54 (ABS_MT_POSITION_Y), value 145
Event: time 20159.696497, ++++++++++++++ SYN_MT_REPORT ++++++++++++
Event: time 20159.696497, -------------- SYN_REPORT ------------
Event: time 20159.728497, type 3 (EV_ABS), code 53 (ABS_MT_POSITION_X), value 728
Event: time 20159.728497, type 3 (EV_ABS), code 54 (ABS_MT_POSITION_Y), value 41
Event: time 20159.728497, ++++++++++++++ SYN_MT_REPORT ++++++++++++
Event: time 20159.728497, type 3 (EV_ABS), code 53 (ABS_MT_POSITION_X), value 595
Event: time 20159.728497, type 3 (EV_ABS), code 54 (ABS_MT_POSITION_Y), value 154
Event: time 20159.728497, ++++++++++++++ SYN_MT_REPORT ++++++++++++
Event: time 20159.728497, type 3 (EV_ABS), code 53 (ABS_MT_POSITION_X), value 456
Event: time 20159.728497, type 3 (EV_ABS), code 54 (ABS_MT_POSITION_Y), value 145
Event: time 20159.728497, ++++++++++++++ SYN_MT_REPORT ++++++++++++
Event: time 20159.728497, -------------- SYN_REPORT ------------
Event: time 20159.760360, ++++++++++++++ SYN_MT_REPORT ++++++++++++
Event: time 20159.760360, -------------- SYN_REPORT ------------
Устройство функционирует в соответствии со стандартом Linux Kernel.
На данный момент существует две проблемы:
- Тачскрин довольно глючно отрабатывает отпущенные нажатия, если их отпускать в случайном порядке. Проблема идет от устройства, и я пока не знаю решения для него
- Решение создает тачскрин, а не адаптирует Xorg для работы с ним. Поэтому нужны дополнительные драйвера для адаптации. Однако для приложений Qt в EGLFS этого и не нужно. Достаточно установить переменную окружения QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS=/dev/input/eventX, где X — номер вашего тачскрина.
Пример приложения из примеров для Qt 5.5 quick/touchinteractions (прошу прощения за отвратительное качество фото).
Если найдете ошибки в драйвере/косяки дизайна — буду рад их принять, поскольку это моя первая «серьезная» программа, написанная на С.
Еще раз ссылка на гитхаб.