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

пример

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

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

© Habrahabr.ru