Linux Embedded

Le blog des technologies libres et embarquées

Une introduction à uinput

Introduction 

Dans notre monde de l'embarqué, il est courant de devoir gérer du hardware "exotique". Parmi ces matériels, les périphériques d'entrées sont un problème récurrent. Les systèmes embarqués étant souvent utilisés dans des contextes "machines" plutôt que "ordinateurs", il est courant que les systèmes de saisie ne soient pas notre classique paire clavier/souris.

Il est donc assez courant de devoir intégrer dans linux des périphériques d'entrée non-standards et ne disposant pas de drivers dédiés. L'écriture de driver linux peut être une solution mais il peut également être intéressant d'interfacer notre périphérique purement en user-space.

Linux fournit un système permettant d'envoyer des évènements input dans le kernel comme si ils provenaient d'un périphérique. Ces évènements seront ensuite traités comme tous les évènements par le noyau, renvoyés vers les programmes de plus haut niveau (console, serveur X, compositeur wayland) et traités de façon transparente.

Outre l'écriture de driver en user-space, cette méthode peut également être utilisée pour de l'enregistrement/du rejeu d'évènements clavier ou pour de la simulation (par exemple dans le cadre de tests automatisés)

Les input sous linux : Une vision user-space

Les différentes façons d'accéder aux périphériques

Tous les périphériques d'entrée sous linux sont gérés par le sous-système input du noyau. Chaque périphérique est accessible par nos applications via les fichiers /dev/input/event*. Nous pouvons ouvrir ces fichiers en lecture en suivant le protocole décrit dans la documentation du noyau.

Pour éviter de travailler directement avec l'API noyau, il existe libevdev qui offre une API un peu plus riche dans différents langages. libevdev permet également de créer des périphériques virtuels et c'est cette librairie que nous utiliserons dans nos exemples (dans sa version python).

Si l'on souhaite interagir avec le système d'input de façon encore plus haut niveau, il est possible d'utiliser libinput. Libinput ajoute une couche d'intelligence au dessus des évènements noyau. Cela résout les problèmes complexes comme la détection de paume, l'émulation du troisième bouton, la calibration des touchpads, etc. Libinput ne sera pas couvert dans cet article, mais c'est un outil intéressant à étudier.

Mais au fait, quels types de périphériques couvre-t-on ?

Le sous-système input regroupe beaucoup de choses. Il y a les choses évidentes (clavier, souris) mais également tout ce qui peut être assimilé à un interrupteur sur votre machine :

  • Tous les types de souris (classique, touchpad, trackpad, écran tactile, tablette graphique, ...)
  • Tous les types de claviers (intégrés, externes, clavier numérique USB, pointeurs laser)
  • Les boutons power 
  • L'interrupteur qui détecte la fermeture de votre écran de portable
  • Les joysticks, gamepads et autres périphériques de jeux
  • Des évènements hardware (branchement d'un casque dans la prise audio)
  • Les télécommandes (avec tous les boutons dédiés pour les menu, les chapitres des DVD)

L'outil evtest permet d'avoir une liste des périphériques d'input présent. Sur un laptop typique, ils sont assez nombreux :

Liste des périphériques sur un laptop

 

Il y a beaucoup de périphériques. Une explication rapide de certains d'entre eux :

  • Le clavier et le trackpad (0 et 10)
  • Différents connecteurs HDMI avec des évènements de connexion, déconnexion (18, 19, 20, 21)
  • Le bouton power (7 et 8, pour l'appui court et long)
  • La charnière de l'écran (6)
  • La sortie PS/2 du CPU (absente de la carte mère, mais linux ne peut pas le savoir, 2)
  • Les boutons des webcams intégrées (qui n'ont pas de bouton, mais à nouveau linux ne peut pas le savoir, 13, 14)
  • Des pseudo-claviers pour gérer les touches multimédia (1, 15)

Qui sait faire quoi ?

L'outil evtest nous servira de base pour comprendre le fonctionnement d'un périphérique de type input. Si nous prenons le périphérique le plus simple (la charnière de laptop), et que nous fermons puis ouvrons notre laptop, nous obtenons la séquence suivante :

évenements charnière

Nous avons d'abord une liste des évènements que notre charnière peut envoyer :

  • Un premier évènement concernant un interrupteur (EV_SW) pour une charnière (SW_LID) qui est actuellement ouverte (state 0)
  • Un évènement SYN_REPORT (que nous détaillerons après)

Puis nous voyons que quatre évènements ont été envoyés par la charnière :

  • Un premier évènement signalant la fermeture de la charnière (value 1)
  • Un évènement SYN_REPORT signalant la fin d'un "paquet d'évènements"
  • Un deuxième évènement signalant la réouverture de la charnière (value 0)
  • Un dernier évènement SYN_REPORT

Notre charnière est le cas le plus simple car il n'y a qu'un seul "vrai" évènement, mais sur des périphériques plus complexes il peut être nécessaire d'envoyer plusieurs évènements "simultanément". Nous pouvons voir un exemple avec une souris

mouse-events

Notre souris expose une liste plus conséquente d'évènements

  • L'évènement EV_SYN permettant de regrouper des événements
  • Des évèmenents de type click (EV_KEY) correspondant aux différents boutons de la souris 
  • Des évènements de type mouvements relatifs (EV_REL) correspondant aux déplacements de la souris (REL_X, REL_Y) et aux molettes de la souris (les quatre REL_WHEEL)
  • Un évènement MSC_SCAN que nous ne détaillerons pas dans cet article

Notons qu'il est assez courant qu'un périphérique expose plus de boutons qu'il n'a véritablement. Ma souris n'a que 5 boutons et non 8 et n'a qu'une molette et non deux.

Lorsque je déplace ma souris, nous observons des évènements de déplacement par paquets de 3 :

  • Un déplacement selon l'axe des X
  • Un déplacement selon l'axe des Y
  • Un évènement SYN_REPORT (ou EV_SYN, c'est la même chose) signalant que les deux évènements précédents doivent être traités comme un tout.

 

Uinput : créons nos propres périphériques

Cette petite présentation de base suffira pour nous permettre de gérer des périphériques simples.

Un petit clavier à deux touches...

Nous allons donc créer un clavier avec deux touches, les touches A et B.

Pour cela, nous allons initialiser un nouveau périphérique avec libevdev, nous indiquons qu'il est équipé des touches A et B puis nous enregistrons le tout auprès du noyau.

#!/usr/bin/python3
import libevdev
from time import sleep

# Creation et parametrage du device
device = libevdev.Device()
device.name = 'Fake keyboard'
device.enable(libevdev.EV_KEY.KEY_A)
device.enable(libevdev.EV_KEY.KEY_B)

uinput = device.create_uinput_device()

# Le kernel a besoin d'une seconde avant l'envoi du premier évenement
sleep(1)

(Note : la liste complète des évènements se trouve dans le fichier /usr/include/linux/input-event-codes.h sur toute machine linux)

Si nous exécutons ce programme et que nous lançons evtest pendant son exécution, nous trouvons bien les propriétés de notre périphérique

fake keyboard properties

Notre clavier a bien les touches A et B. Le noyau a automatiquement ajouté l'évènement SYN.

Ajoutons un peu de code pour émettre des appui touches :

pressA = [libevdev.InputEvent(libevdev.EV_KEY.KEY_A, value=1),
          libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, value=0)]
pressB = [libevdev.InputEvent(libevdev.EV_KEY.KEY_B, value=1),
          libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, value=0)]
releaseA = [libevdev.InputEvent(libevdev.EV_KEY.KEY_A, value=0),
            libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, value=0)]
releaseB = [libevdev.InputEvent(libevdev.EV_KEY.KEY_B, value=0),
            libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, value=0)]

for i in [pressA, releaseA,pressB,releaseB]:
    uinput.send_events(i)

Un clavier envoie un évènement lors de l'appui d'une touche et un autre évènement lors du lâcher de la touche. A chaque évènement, nous devons envoyer deux messages simultanément :

  • l'évènement "clavier" lui-même
  • Un évènement SYN_REPORT indiquant la fin du groupe d'évènements (obligatoire)

La fonction uinput.send_events(<list>) sert à envoyer plusieurs évènements simultanément, exprimés sous forme de tableau. Nous avons fait quatre tableaux de deux évènements correspondants à ce que nous voulons générer (pressA, pressB, releaseA, releaseB) et nous les avons envoyés tour à tour dans notre périphérique.

Sans surprise, evtest voit bien arriver des évènements clavier :

keyboard events

Deux petites choses à noter dans cette capture d'écran :

  • Les lettres "ab" après le dernier SYN_REPORT qui correspondent à l'effet des appuis des touches dans le terminal (les évènements sont absorbés par wayland et poussés jusqu'au terminal)
  • Les erreurs après les évènements : le programme evtest ne gère pas proprement la disparition des périphériques, mais notre code est néanmoins correct.

Plus compliqué : le trackpad

Tous les périphériques de type "pad" (tablettes graphiques, trackpad, écrans tactiles) renvoient les coordonnées des contacts dans leur zone active. Pour les écrans tactiles, ces coordonnés représentent des coordonnés à l'écran. Pour un trackpad, bien qu'absolue, la position n'est pas véritablement significative.

Nous allons simuler un doigt qui se déplace sur un trackpad. Pour cela nous allons devoir générer deux types d'évènements :

  • Un évènement indiquant quel outil est "à proximité" (et sera donc utilisé prochainement).
    Dans notre cas il s'agira du BTN_TOOL_FINGER qui indique un doigt sur un trackpad, mais d'autres évènements indiquant que plusieurs doigts sont en contact ou des outils plus évolués pour les tablettes graphiques (gomme par exemple)
  • Un évènement indiquant que l'outil est en contact : BTN_TOUCH

Nous enverrons ces évènements simultanément, mais un driver plus complexe pourrait les différencier.

Tout d'abord, configurons notre périphérique :

#!/usr/bin/python3
import libevdev
from time import sleep


### Creation et parametrage du device
device = libevdev.Device()
device.name = 'fake trackpad'
device.enable(libevdev.EV_KEY.BTN_TOOL_FINGER)
device.enable(libevdev.EV_KEY.BTN_TOUCH)
device.enable(libevdev.EV_ABS.ABS_X,libevdev.InputAbsInfo(minimum=0, maximum=100, resolution=1))
device.enable(libevdev.EV_ABS.ABS_Y,libevdev.InputAbsInfo(minimum=0, maximum=100, resolution=1))
device.enable(libevdev.INPUT_PROP_POINTER)

uinput = device.create_uinput_device()

sleep(1) 

Nous indiquons nos deux évènements liés à l'appui des touches et précisons que nous allons également générer deux évènements de type "coordonnées absolues" (EV_ABS) qui représentent des mouvement horizontaux (ABS_X) et verticaux (ABS_Y)

Nous précisons également les valeurs minimum et maximum à attendre ainsi que la résolution. Dans notre cas, je me suis contenté de valeurs entre 0 et 100 avec une résolution de 1. Ce serait bien insuffisant pour un vrai périphérique, mais cela suffira ici.

Enfin, nous utilisons la propriété INPUT_PROP_POINTER pour indiquer que ce périphérique a besoin que le curseur reste affiché pendant son utilisation (contrairement à un écran tactile par exemple).

Vérifions ce que evtest voit de notre périphérique :

propriétés de notre trackpad virtuel

Il ne nous reste plus qu'à déplacer notre pointeur :


touch_the_pad =[
        libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, value=1),
        libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_FINGER, value=1),
        libevdev.InputEvent(libevdev.EV_ABS.ABS_X, value=0),
        libevdev.InputEvent(libevdev.EV_ABS.ABS_Y, value=50),
        libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, value=0),
                            ]
uinput.send_events(touch_the_pad)

for i in range(10,50,5):
    move_finger =[
            libevdev.InputEvent(libevdev.EV_ABS.ABS_X, value=i),
            libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, value=0),
            ]
    uinput.send_events(move_finger)
    sleep(0.2)

remove_finger = [
        libevdev.InputEvent(libevdev.EV_KEY.BTN_TOUCH, value=0),
        libevdev.InputEvent(libevdev.EV_KEY.BTN_TOOL_FINGER, value=0),
        libevdev.InputEvent(libevdev.EV_SYN.SYN_REPORT, value=0),
        ]

uinput.send_events(remove_finger)

Nous avons ici trois blocs d'évènements :

  • L'appui du doigt, qui consiste à
    • Indiquer que l'outil utilisé est le doigt (EV_BTN_TOOL_FINGER à 1)
    • Indiquer que celui-ci est en contact du trackpad (EV_BTN_TOUCH à 1)
    • Indiquer la position de cet appui (ABS_X et ABS_Y)
  • Le déplacement lui-même, une boucle qui envoie des évènements ABS_X avec des coordonnés croissantes (mouvement purement horizontal)
  • La fin de l'appui du doigt qui consiste à envoyer les évènements inverses de l'appui

evtest nous affiche alors les évènements suivants :

mouvement de la souris

et, plus important, notre curseur se déplace tout seul à l'écran.

Conclusion

Créer un périphérique virtuel à partir d'évènements logiciels est trivial. Même un périphérique un peu complexe comme un trackpad peut être facilement géré en quelques lignes. La description des propriétés est simple à mettre en place, l'outillage de test est facile à utiliser et l'envoi des évènements se comprend très rapidement.

Cet article a pour but premier de démystifier l'intégration de périphériques inhabituels (sur un port série, à travers un réseau, un bus CAN, ou qui génèrent du XML) et de montrer que le plus simple n'est pas de gérer ces périphériques dans vos applications métiers mais bien de développer un driver user-space qui réinjecte les évènements dans le noyau et qui permet d'utiliser ces périphériques comme n'importe quel dispositif de pointage classique.

Pour aller plus loin

Nous avons montré les cas les plus simples d'input mais l'API noyau couvre de nombreux cas :

  • Les périphériques émettant des déplacements relatifs, comme les souris
  • La gestion de la pression sur la surface tactile
  • La forme du doigt 
  • La gestion des doigts multiples, avec un suivi indépendant de chaque doigt
  • L'envoi d'information vers le périphérique d'input (comme les LED d'un clavier ou un bip par exemple)
  • La gestion du force-feedback 

Tout ceci est bien documenté ici et est relativement simple. N'hésitez pas à explorer cette documentation !

 

 

 

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.