Hace tiempo me compré una tableta de dibujo de estas para «pintar con lápiz», pero por desgracia nunca la pude usar porque no funcionaba correctamente en Linux: al mover el lápiz, el cursor del ratón se movía correctamente, pero no hacía nada cuando tocaba sobre la superficie ni cuando pulsaba los botones del lápiz. Sin embargo, como estos días tenía algo de tiempo libre, decidí ponerme con ella a ver qué podía descubrir, y si conseguía resolver el problema.
Lo primero que hice fue echar un vistazo a la información de lsusb. Ahí puede ver el código del fabricante y el del dispositivo:
Bus 002 Device 006: ID 2179:0004 UGTABLET TABLET WP5540
Una búsqueda rápida en una de las muchas bases de datos de USB me mostró que el chip controlador pertenece a la empresa UGtizer Corp, pero en su web no encontré ningún driver adecuado. Una búsqueda en la página de USB Linux tampoco me dio información útil.
Decidí entonces que podría probarla en Windows, a ver si era un problema de la tableta en sí. Para ello instalé uno en una máquina virtual y configuré un filtro USB para que redirigiese la tableta a éste tan pronto se enchufase, y evitar así interferencias desde Linux.
Y funcionó perfectamente: si movía el lápiz a unos milímetros de la superficie, el cursor se movía igual que en Linux, pero si lo apoyaba, Windows identificaba una pulsación de botón de ratón y podía seleccionar iconos, cosa que en Linux no ocurría. Los dos botones del lápiz también funcionaban como el botón derecho y central del ratón.
Por supuesto, dado que la tablet medio funcionaba en Linux, yo supuse que debía seguir el estándar USB HID, pero que habría algo que se salía de él y Linux no era capaz de identificar correctamente. O bien podría ser que hubiese que configurar primero de alguna manera la tableta, y que Windows lo estuviese haciendo bien y Linux no.
Esnifando paquetes USB
Decidí comprobar primero la segunda posibilidad. Para ello desenchufé primero la tableta y cargué el módulo usbmon con:
sudo modprobe usbmon
Luego lancé Wireshark, empecé a capturar desde el puerto 2 de USB (pues, si vemos la salida de lsusb, es en él en donde está el puerto que usé), conecté la tableta, e hice algunos movimientos con el lápiz sin apoyar y apoyado en la superficie.
Hecho esto, detuve la captura, desconecté la tableta, apagué la máquina virtual, e hice una nueva captura, para ver qué hacía Linux. Luego abrí ambas capturas, cada una en una ventana diferente, y filtré los paquetes de la tableta con:
usb.bus_id == X&& usb.device_address == YY
Siendo X el bus en el que estaba conectada la tableta, e YY el identificador que se le asignó en cada momento.
El resultado no fue nada conclusivo, pues en ambas capturas salía más o menos lo mismo: primero se piden los datos básicos del dispositivo, luego la configuración completa (donde pone que es un dispositivo HID), algunas cosas más sin importancia, y finalmente lee el HID Report, donde vienen los datos HID para configurar el dispositivo. Tanto en Windows como en Linux eran exactamente iguales, por lo que no parecían ir por ahí los tiros. Una vez hecho todo esto, el resto de datos se enviaban sólo desde la tableta hacia el ordenador en forma de interrupciones USB, y parecían bastante similares entre ambos sistemas operativos.
Llegados a este punto no podía hacer mucho más, porque no sabía nada sobre el estándar HID ni sobre el significado de cada byte que se estaba enviando sobre el cable, así que decidí hacer un alto y leerme la documentación oficial de HID (una lectura amena y tremendamente divertida, como cualquier documentación oficial 😝 ).
El estándar HID
La idea detrás de HID es crear un protocolo que permita definir absolutamente cualquier dispositivo de interfaz humana, tanto actual como futuro, y hacerlo además de una manera que permita que cada fabricante lo implemente casi de la manera que quiera. Para ello, un dispositivo HID simplemente tiene que definir un HID Report, que no es más que una tabla donde se definen qué tipos de entrada tiene, y poco más. Dicha tabla puede estar definida directamente en ROM, lo que simplifica mucho el diseño.
¿Y qué contiene dicha tabla? Pues básicamente una serie de entradas en las que se define, para cada posible sensor o actuador del dispositivo:
- Tipo (por ejemplo: Eje X relativo, Tecla Y, presión…)
- Número de bits que ocupa el dato (el bit inicial se deduce de la propia lista)
- Rango físico y lógico
Y algunas cosas más. Además, las entradas se pueden agrupar por tipo de dispositivo, de manera que no es lo mismo un botón de ratón que un botón de teclado, y asignarle un identificador a cada grupo.
En el caso de mi tableta, la tabla HID es así:
- Uso digitizer (id: 0x07)
- Punta de lápiz: 1 bit
- Botón de lápiz: 1 bit
- Goma de borrar de lápiz: 1 bit
- No usados: 2 bits
- Lápiz dentro de rango: 1 bit
- No usados: 2 bits
- Eje X: 16 bits
- rango físico: 0, 5500
- rango lógico: 0, 22000
- Eje Y: 16 bits
- rango físico: 0, 4000
- rango lógico: 0, 16000
- Uso mouse (id: 0x08)
- Botón 1: 1 bit
- Botón 2: 1 bit
- Botón 3: 1 bit
- No usados: 5 bits
- Eje X relativo: 8 bits
- rango lógico: -127, 127
- Eje Y relativo: 8 bits
- rango lógico: -127, 127
- Rueda de ratón: 8 bits
- rango lógico: -127, 127
- No usados: 8 bits
- Uso mouse (id: 0x09)
- Botón 1: 1 bit
- Botón 2: 1 bit
- Botón 3: 1 bit
- No usados: 5 bits
- Eje X absoluto: 16 bits
- rango lógico: 0, 32767
- Eje Y absoluto: 16 bits
- rango lógico: 0, 32767
- Presión del lápiz: 16 bits
- rango lógico: 0, 1023
Con esta tabla a mano, si nos llega un bloque de interrupción con los siguientes valores:
09 00 5d 49 05 56 00 00
Podemos deducir fácilmente su significado: el primer byte es el uso, por lo que, al ser 9, tenemos que fijarnos en el último de los tres bloque de la tabla. El primer byte contiene el estado de tres botones de tipo ratón, pero como los bits correspondientes son 0, significa que no están pulsados. Los dos siguientes bytes son la posición del lápiz en el eje X, los dos siguientes la posición en el eje Y, y los dos últimos la presión. Fácil.
La cuestión es que tanto en Windows como en Linux los datos que se enviaban eran exactamente iguales: si tocaba con el lápiz en la superficie se activaba el bit 0 del primer byte y aparecía un valor en los dos últimos bytes, que aumentaba con la presión que ejercía. Si pulsaba alguno de los dos botones en el lápiz se activaban los bits 1 o 2 del primer byte… todo era correcto, salvo por el detalle de que Linux no hacía caso. Incluso escribí un pequeño programa en python para leer e interpretar los datos en tiempo real desde la interfaz usbmon, y todo era correcto: los datos se enviaban correctamente, y siempre era con bloques de tipo 9.
De hecho, no podía evitar preguntarme por qué había dos entradas de tipo ratón, y sobre todo por qué una de ellas tenía ejes relativos. La respuesta se me ocurrió de casualidad: este chip sirve para tabletas híbridas, donde se puede usar un lápiz o un ratón:
Eso explicaba los dos tipos: si se usaba el ratón, se emitirían interrupciones con el tipo 0x08, y si se usa el lápiz entonces serían de tipo 0x09. Y el tipo 0x07 supongo que sería para algún tipo de lápiz diferente.
Vamos al kernel
Llegados a este punto comprendí que el problema tenía que estar, necesariamente, en el driver HID de Linux, así que descargué las fuentes del kernel, las compilé y las instalé en mi Debian con:
make localmodconfig # copia la configuración del kernel actual
make -j5
make deb-pkg -j5 # crea paquetes .deb
Y con ello, me fui al fichero hid-input.c. En él encontré el punto en el que procesa la tabla HID y descubrí un detalle interesante hacia el final de la función:
¡Si un evento estaba duplicado, parecía ignorarlo a menos que se activase HID_QUIRK_INCREMENT_USAGE_ON_DUPLICATE! Esto tenía buena pinta, porque, efectivamente, los eventos de pulsación de ratón están duplicados en el bloque 8 y en el 9. Probé a añadir el Quirk para mi tableta en el fichero hid-quirks.c, compilé, desmonté los módulos de HID y volví a montarlos, y…
¡CASI! Me había asignado al lápiz las siguientes tres acciones del ratón: siguiente página, anterior página y no-se-cual-más. Claramente iba por buen camino.
Se me ocurrió entonces modificar el código para invertir el orden en el que se procesaban las entradas de la tabla HID, de manera que primero se añadiesen las del bloque 9 y luego las del 8, y el resultado fue que… ¡¡¡FUNCIONÓ!!! Parecía que lo había resuelto, así que creé un parche para poder configurarlo como un Quirk más, añadí mi tableta a la lista de dispositivos con Quirks, y envié un parche a la gente del kernel. En respuesta, amablemente me indicaron que mi solución se veía demasiado alambicada, y que quizás debería probar con HID_QUIRK_MULTI_INPUT, que crearía un dispositivo diferente para cada tipo de bloque. Lo probé, y efectivamente, esa era la solución, así que creé un nuevo parche, mucho más sencillo, lo envié, y fue aceptado. ¡¡Hurra!!
Añadiendo un QUIRK a un dispositivo en el driver HID
Vamos a ver este detalle con más calma porque es importante, y puede serle útil a más gente. Lo primero que tenemos que hacer es bajarnos las fuentes del kernel actual desde el repositorio GIT oficial:
git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
Una vez que lo tenemos, nos hacemos un branch para nuestro parche y abrimos el fichero drivers/hid/hid-ids.h. En él buscamos si el fabricante de nuestro dispositivo ya está añadido o no (lo más probable es que sí), mediante el identificador que obtuvimos con lsusb. En mi caso, el identificador 0x2179 ya estaba añadido a nombre de UGTizer con:
#define USB_VENDOR_ID_UGTIZER 0x2179
pero dentro de él no estaba mi tableta, por lo que añadí una línea con su definición:
#define USB_DEVICE_ID_UGTIZER_TABLET_WP5540 0x0004
Una vez hecho esto sólo tuve que abrir el fichero drivers/hid/hid-quirks.c y añadir una entrada con mi vendor, mi device y los quirks que quería añadir para él:
{ HID_USB_DEVICE(USB_VENDOR_ID_UGTIZER, USB_DEVICE_ID_UGTIZER_TABLET_WP5540), HID_QUIRK_MULTI_INPUT },
Y con esto estuvo listo.
¿Y mientras espero a que mi distro añada el parche?
Obviamente no quería estar con el kernel de desarrollo en mi sistema, sino que quería tener el oficial. ¿Significa eso que no podré usar mi tableta hasta que Debian haga un backport del parche? Pues afortunadamente no, porque el módulo HID permite pasarle manualmente un grupo de quirks a añadir a un grupo de dispositivos. Para ello basta con editar el fichero /etc/default/grub y, en la línea donde se definen los parámetros de arranque del kernel (que, en general, será la de GRUB_CMDLINE_LINUX_DEFAULT) añadir lo siguiente:
usbhid.quirks=0x2179:0x0004:0x0040
El primer número hexadecimal es el identificador USB del fabricante, el segundo es el identificador de dispositivo, y el tercero es un mapa de bits con los quirks a activar. La lista está definida en el fichero drivers/hid/hid.h, y en el momento de escribir este artículo es:
#define HID_QUIRK_INVERT BIT(0)
#define HID_QUIRK_NOTOUCH BIT(1)
#define HID_QUIRK_IGNORE BIT(2)
#define HID_QUIRK_NOGET BIT(3)
#define HID_QUIRK_HIDDEV_FORCE BIT(4)
#define HID_QUIRK_BADPAD BIT(5)
#define HID_QUIRK_MULTI_INPUT BIT(6)
#define HID_QUIRK_HIDINPUT_FORCE BIT(7)
/* BIT(8) reserved for backward compatibility, was HID_QUIRK_NO_EMPTY_INPUT */
/* BIT(9) reserved for backward compatibility, was NO_INIT_INPUT_REPORTS */
#define HID_QUIRK_ALWAYS_POLL BIT(10)
#define HID_QUIRK_INPUT_PER_APP BIT(11)
#define HID_QUIRK_X_INVERT BIT(12)
#define HID_QUIRK_Y_INVERT BIT(13)
#define HID_QUIRK_SKIP_OUTPUT_REPORTS BIT(16)
#define HID_QUIRK_SKIP_OUTPUT_REPORT_ID BIT(17)
#define HID_QUIRK_NO_OUTPUT_REPORTS_ON_INTR_EP BIT(18)
#define HID_QUIRK_HAVE_SPECIAL_DRIVER BIT(19)
#define HID_QUIRK_INCREMENT_USAGE_ON_DUPLICATE BIT(20)
#define HID_QUIRK_FULLSPEED_INTERVAL BIT(28)
#define HID_QUIRK_NO_INIT_REPORTS BIT(29)
#define HID_QUIRK_NO_IGNORE BIT(30)
#define HID_QUIRK_NO_INPUT_SYNC BIT(31)
Algunos son obvios, pero otros, la verdad, es que no se para qué pueden servir, así que si algún día me encuentro con un dispositivo HID que no va, iré probando de uno en uno a ver.
Haciendo que mi tableta de dibujo funcione en Linux por A cuadros está licenciado bajo una Licencia Creative Commons Atribución-CompartirIgual 4.0 Internacional.
Muy buen articulo, acabo de descubrir tu blog y me encanta!! Sigue asi.
Salu2 de un aprendiz de SysAdmin 🙂