Пишем драйвер пользовательского окружения для 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 — разделитель

Найденный драйвер может и быстрое решение, но мне не понравился по следующим причинам:

  1. Мне кажется писать драйверы на интерпретируемом языке в Embedded не лучшей идей
  2. Драйвер имитирует поведение мыши, и игнорирует мультитач

Было принято решение устроить себе челендж и написать свой драйвер для uinput на C с мультитачем и udev’ом.

Для ленивых: исходники сырой версии драйвера лежат тут.

Решение проблемы

Прежде всего нужно осознавать, что все, что втыкается в USB можно и выткнуть, а поэтому нужно учитывать горячее подключение к порту.
В линуксе есть замечательная библиотека libudev, которая позволяет произвести перечисление подключенных устройств и мониторить добавление/удаление устройств.
Один из недостатков нашего случая — драйвер hidraw не несет информации о VendorId:ProductId устройства USB, к которому он привязан. Поэтому нужно делать перечисление устройств по драйверу hidraw, а затем искать родительское USB-устройство с указанными нами идентификаторами.
Важно: если мы хотим проверить уже подключенные устройства и следить за добавлением/удалением, необходимо выполнить действия в следующем порядке (и никак иначе):

  1. Инициализировать udev_monitor
  2. Запросить перечисление подключенных устройств udev_enumerate_get_list_entry
  3. Запустить цикл опроса 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 (прошу прощения за отвратительное качество фото)

пример

Если найдете ошибки в драйвере/косяки дизайна — буду рад их принять, поскольку это моя первая «серьезная» программа, написанная на С.

Еще раз ссылка на гитхаб.

ссылка на оригинал статьи http://habrahabr.ru/post/267655/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *