2019-02-21
md
Prise en charge d'une télécommande IR avec python-evdev
<-Télécommandes à infrarouge sur l'Orange Pi Zero; le matériel

Ce billet est consacré à l'utilisation des télécommandes à infrarouge sur un Orange Pi Zero à l'aide d'une bibliothèque Python, python-evdev, crée par Georgi Valkov. Si l'exemple qu'on retrouve en fin de ce papier roule actuellement sur un Orange Pi Zero exécutant Armbian Stretch, presque tout ce qu'on retrouve ici est valable pour le Raspberry Pi avec Raspbian Stretch. D'ailleurs la révision de ce qui suit a été faite avec un Raspberry Pi 3 auquel était branché un récepteur IR connecté par USB, car le Orange Pi Zero est déjà en service.

Avant d'utiliser python-evdev, il faut s'assurer que télécommandes IR qu'on veut utiliser sont prises en charge par le noyau Linux. Il serait aussi préférable d'avoir une table de traduction de codes de balayage en codes de clavier pour chaque télécommande, mais ce n'est pas absolument nécessaire. Consultez le billet précédent Configuration de télécommandes à infrarouge sur l'Orange Pi Zero pour les détails.

Table des matières

  1. Prérequis Python
  2. Gestion des événements IR
  3. Première utilisation de python-evdev
  4. Exemple pratique avec Domoticz
  5. Conclusion

Prérequis Python toc

Python 3.5 est installé par défaut dans Armbian Stretch. Ce n'est cependant pas le cas pour pip3 le programme d'installation de paquets Python et ses outils associés qui seront utilisés pour installer la bibliothèque nécessaire.

zero@opi:~$ sudo apt install -y python3-pip python3-dev python3-setuptools python3-wheel ... 0 upgraded, 11 newly installed, 0 to remove and 0 not upgraded. Need to get 40.7 MB of archives. After this operation, 55.3 MB of additional disk space will be used. ... Setting up python3-dev (3.5.3-1) ...

Maintenant pip3 (pip pour Python 3) peut être utilisé pour obtenir la bibliothèque evdev.

zero@opi:~$ pip3 install evdev Collecting evdev Using cached https://files.pythonhosted.org/packages/7e/53/374b82dd2ccec240b7388c65075391147524255466651a14340615aabb5f/evdev-1.1.2.tar.gz ... Successfully installed evdev-1.1.2

C'est la seule bibliothèque qui sera utilisée.

Gestion des événements IR toc

La documentation de la bibliothèque evdev est disponible sur Read the Docs. Je vais commencer par une session interactive Python pour tout tester avec la télécommande KEYES. Je commence en effaçant tous les enregistrements de conversion de codes de balayages en codes de clavier et en utilisant le bon protocole pour la télécommande.

zero@opi:~$ sudo ir-keytable -c -p nec zero@opi:~$ python3 Python 3.5.3 (default, Sep 27 2018, 17:25:39) [GCC 6.3.0 20170516] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import evdev >>> devices = [evdev.InputDevice(path) for path in evdev.list_devices()] >>> for device in devices: ... print(device.path, device.name, device.phys) ... il faut appuyer deux fois sur Entrée! /dev/input/event0 sunxi-ir sunxi-ir/input0 >>> device = evdev.InputDevice('/dev/input/event0') >>> print(device) device /dev/input/event0, name "sunxi-ir", phys "sunxi-ir/input0" >>> for event in device.read_loop(): ... print(event) ... il faut appuyer deux fois sur Entrée! event at 1549134307.050319, code 04, type 04, val 25 event at 1549134307.050319, code 00, type 00, val 00 event at 1549134307.101433, code 04, type 04, val 25 event at 1549134307.101433, code 00, type 00, val 00 event at 1549134307.209101, code 04, type 04, val 25 event at 1549134307.209101, code 00, type 00, val 00 event at 1549134309.079462, code 04, type 04, val 24 event at 1549134309.079462, code 00, type 00, val 00 event at 1549134309.130551, code 04, type 04, val 24 event at 1549134309.130551, code 00, type 00, val 00 ^CTraceback (most recent call last): File "<stdin>", line 1, in <module> File "/home/zero/.local/lib/python3.5/site-packages/evdev/eventio.py", line 45, in read_loop r, w, x = select.select([self.fd], [], []) KeyboardInterrupt

Appuyez sur la combinaison de touches CtrlC pour interrompre la boucle. Le premier champ affiché par evdev lorsqu'un événement est lu du périphérique d'entrée input0 est un code temporel, puis viennent les champs code, type et value. Tout cela a déjà été vu avec evtest (voir Obtenir les codes de balayage de la télécommande) et avec ir-keytable -t (voir L'utilitaire ir-keytable). Les marques de synchronisation (EV_SYN) ne sont pas très utiles et elles peuvent être escamotées avec le reste de l'information pour n'afficher que les codes de balayage des boutons activés.

>>> from evdev import InputDevice, ecodes >>> device = InputDevice('/dev/input/event0') >>> for event in device.read_loop(): ... if event.type == ecodes.EV_MSC: ... print(event.value) ... il faut appuyer deux fois sur Entrée! 25 25 25 24 24 ^CTraceback (most recent call last): File "<stdin>", line 1, in <module> File "/home/zero/.local/lib/python3.5/site-packages/evdev/eventio.py", line 45, in read_loop r, w, x = select.select([self.fd], [], []) KeyboardInterrupt >>> quit() zero@opi:~$

Ce qui précède ne contient que des variantes de quelques-uns des sujets abordés dans le tutoriel rédigé par Georgi Valkov.


Première utilisation de python-evdev toc

Ce simple script lira tous les événements du périphérique d’entrée et affichera le nom des boutons enfoncés sur la télécommande KEYES en fonction de leurs codes de balayage uniquement.

#!/usr/bin/python3 from evdev import InputDevice irr = InputDevice('/dev/input/event0') print("Press remote IR buttons, Ctrl-C to quit") for event in irr.read_loop(): if event.type == 4: try: if event.value == 82: print("button pressed: 0") elif event.value == 22: print("button pressed: 1") elif event.value == 25: print("button pressed: 2") elif event.value == 13: print("button pressed: 3") elif event.value == 12: print("button pressed: 4") elif event.value == 24: print("button pressed: 5") elif event.value == 94: print("button pressed: 6") elif event.value == 8: print("button pressed: 7") elif event.value == 28: print("button pressed: 8") elif event.value == 90: print("button pressed: 9") elif event.value == 64: print("button pressed: OK") elif event.value == 70: print("button pressed: UP") elif event.value == 21: print("button pressed: DOWN") elif event.value == 68: print("button pressed: LEFT") elif event.value == 67: print("button pressed: RIGHT") elif event.value == 66: print("button pressed: *") elif event.value == 74: print("button pressed: #") else: print(event) except: print("Problem with key press...")

Version pouvant être téléchargée: test_ir.py.

Si j'avais écrit cela en Pascal, j'aurais créé un tableau d'enregistrements, avec le code de balayage comme premier champ et le nom du bouton comme deuxième champ. Le tableau serait trié selon le code de balayage et une recherche binaire serait utilisée pour trouver le nom du bouton en fonction du code lue. Il me reste à trouver le moyen idiomatique de faire cela en Python.

Voici la production du script. On note que la table de conversion est vidée et que le bon protocole IR pour la télécommande est fixé avant d'exécuter le programme.

zero@opi:~$ sudo ir-keytable -c -p nec zero@opi:~$ python3 test_ir.py Press remote IR buttons, Ctrl-C to quit button pressed: 6 button pressed: 6 button pressed: 6 button pressed: 5 button pressed: 5 button pressed: OK button pressed: OK button pressed: OK ^CTraceback (most recent call last): File "test_ir.py", line 9, in <module> for event in irr.read_loop(): File "/home/zero/.local/lib/python3.5/site-packages/evdev/eventio.py", line 45, in read_loop r, w, x = select.select([self.fd], [], []) KeyboardInterrupt zero@opi:~$

Le script peut se comporter comme s'il s'agissait d'un programme. Sous Windows, l’extension .py devrait suffire, car elle devrait être associée à l’interpréteur Python dans le registre. L'extension ne joue aucun rôle dans Linux, mais il faut que le fichier soit marqué comme exécutable et le « shebang » qui commence avec #! qui identifie le programme qui exécutera le script doit être la première ligne du fichier.

zero@opi:~$ chmod +x test_ir.py zero@opi:~$ ./test_ir.py Press remote IR buttons, Ctrl-C to quit ...

Il existe une version asynchrone du script. En exécutant htop dans une deuxième session pour surveiller chaque script, j'ai été surpris de constater que la version asynchrone semblait un peu plus lente quand un bouton était activé. Cependant, aucune des deux versions ne prenait beaucoup de temps processeur quand la télécommande n'était pas utilisée et que le traitement d'une séquence d'événements liés à l'activation d'un bouton prenait à peine 1% du temps processeur dans la version asynchrone plus lente.

Exemple pratique avec Domoticz toc

On pourrait facilement modifier l'exemple précédent pour envoyer des requêtes HTML au serveur de domotique Domoticz pour qu'il bascule l'état de dispositifs IdO. Ce ne serait pas très pratique. Qu'arriverait-il s'il y avait plus de dispositifs que de boutons sur la télécommande? Et qu'arriverait-il si l'on remplaçait la télécommande.

Vu le billet précédent, la solution au deuxième problème est évidente. Il est préférable d'utiliser les codes clavier plutôt que les codes de balayage. Alors si l'on change de télécommande, il suffit d'utiliser sa table de conversion de codes avec ir-keytable, quitte à la modifier si nécessaire.

Pour ce qui est du premier problème, on peut entrer un nombre d'un ou de plusieurs chiffres et affecter un bouton (OK est tout désigné sur la télécommande KEYES) à l'envoi de la requête. Ou l'on pourrait faire comme les télécommandes de plusieurs téléviseurs et décodeurs; si aucun bouton n'est activé pendant un certain temps, la chaîne dont le nombre est déjà entré est affichée. Je n'aime pas tellement cette dernière façon de faire, ayant souvent entré un 896 plutôt que 563 dans le noir. Le décodeur prend un temps fou à m'informer que je ne suis pas abonné à la chaîne 896. J'ai donc opté pour l'autre approche dans le script Python 3 que j'utilise actuellement. Avant d'examiner le code, voici ce qu'il affiche à la console en mode verbeux, alors que je veux basculer les dispositifs 5 puis 10. L'indice du dispositif est affiché à chaque activation d'un bouton sur la télécommande.

zero@opi:~$ sudo ir-keytable -c -p nec -w /etc/rc_keymaps/keyes Read keyes table Old keytable cleared Wrote 17 keycode(s) to driver Protocols changed to nec zero@opi:~$ ./ir-rem.py nestor@homeserver:~ $ sudo ir-keytable -c -p nec -w /etc/rc_keymaps/keyes nestor@homeserver:~ $ ./ir-rem.py ir-rem.py - Controlling Domoticz IoT devices with an IR remote control Enter Domoticz device number, press OK to toggle it on/off, ^C to quit bouton 5 de la télécommande activé idx: 5 bouton OK de la télécommande activé sending url... http request: http://192.168.1.22:8080/json.htm?type=command&param=switchlight&idx=3&switchcmd=Toggle { "status" : "OK", "title" : "SwitchLight" } ...done idx: 0 bouton 1 de la télécommande activé idx: 1 bouton 0 de la télécommande activé idx: 10 bouton OK de la télécommande activé sending url... http request: http://192.168.1.22:8080/json.htm?type=command&param=switchlight&idx=65&switchcmd=Toggle { "status" : "OK", "title" : "SwitchLight" } ...done idx: 0

Voici le code source qu'on peut aussi télécharger: ir_rem.py.

#!/usr/bin/python3 from evdev import InputDevice, ecodes, KeyEvent import urllib.request # Domoticz idx LampeSurPied = 1 LampeBibliotheque = 3 LampeSurTable = 4 LampeChevet = 5 LampeBureau = 6 GarageInterieur = 7 GarageExterieur = 8 Entree = 24 LampesTV = 52 Plafonnier = 65 LampeSofa = 72 url_json = "http://192.168.1.22:8080/json.htm?type=command&param=switchlight&idx=" toggle_json = "&switchcmd=Toggle" # List of Domoticz idx in order of device number # List of Domoticz idx in order of device number idxList = [LampesTV, LampeSofa, LampeSurPied, LampeSurTable, LampeBibliotheque, GarageExterieur, GarageInterieur, LampeChevet, LampeBureau, Entree, Plafonnier] verbose = 1 # 0 no console output, 1 to print information to console if verbose: print('ir-rem.py - Controlling Domoticz IoT devices with an IR remote control') print() print('Enter Domoticz device number, press OK to toggle it on/off, ^C to quit') # Index into idxList of device to be toggled. The value is entered with # IR remote numeric buttons. Note that deviceIndex is in the range 1 - len(idxList) # and a value of 0 means no index has been specified deviceIndex = 0 # Show the current value of the deviceIndex. # # As implemented, this writes the deviceIndex to the console # # Eventually, this could write deviceIndex on an LCD # in which case deviceIndex == 0 show nothing # deviceIndex >= len(idxList) show ??? # else show deviceIndex number (and a label perhaps) # def showDeviceIndex(): if verbose: print('idx: ',deviceIndex) # Show that a URL request is being sent after the OK button is pressed # on the IR remote # # Currently turns on the on board LED read and write a message on the console # # Eventually, could invert the deviceIndex on the LCD, or change its # colour # def showStartRequest(): if verbose: print('sending url...') # Show that a URL request has been sent after the OK button was pressed # on the IR remote # # Currently writes a message on the console # # Eventually, could erase the LCD # def showEndRequest(): if verbose: print('...done') device = InputDevice('/dev/input/event0') # Main event loop # Only react to event.type == EV_KEY, and event.value == key_up # (key_down and key_hold (i.e. repeat) ignored for event in device.read_loop(): # print(event) if (event.type == ecodes.EV_KEY) and (event.value == KeyEvent.key_up): # If KEY_OK keycode is received then # if deviceIndex is valid then send url to Domoticz to toggle device # reset deviceIndex no matter if url sent or not if event.code == ecodes.KEY_OK: if (deviceIndex > 0) and (deviceIndex <= len(idxList)): showStartRequest() cmd = url_json + str(idxList[deviceIndex-1]) + toggle_json if verbose: print('http request: ', cmd) hf = urllib.request.urlopen(cmd) if verbose: print(hf.read().decode('utf-8')) showEndRequest() deviceIndex = 0 # If a numeric button was pressed then append the corresponding digit # to the deviceIndex. If some other button was pressed, reset the # deviceIndex else: if event.code == ecodes.KEY_0: deviceIndex = deviceIndex*10 elif (event.code >= ecodes.KEY_1) and (event.code < ecodes.KEY_0): deviceIndex = deviceIndex*10 + event.code - ecodes.KEY_1 + 1 else: deviceIndex = 0 showDeviceIndex()

Le code n'est pas complexe. Il ne contient qu'une boucle qui normalement s'exécute sans fin. Au début de la boucle, tout événement disponible du périphérique d'entrée est lu. S'il est de type EV_KEY, c'est-à-dire si c'est un code de clavier produit par le noyau en réponse à un code de balayage provenant du relâchement d'un bouton de la télécommande alors on en tient compte. Si le bouton était KEY_OK et si le nombre saisi à partir de la télécommande est un indice valide alors une requête HTTP est envoyée à Domoticz. Si le bouton était un chiffre alors le nombre déjà saisi est multiplié par 10 et le chiffre est ajouté au nombre. Si un bouton autre qu'un chiffre est saisi, l'indice est remis à zéro.

Ce script n'est qu'un exemple. Il ne devrait pas contenir les indices des dispositifs Domoticz à contrôler. Ces indices devraient est consignés dans un fichier de configuration pour qu'il soit possible de modifier la liste de dispositifs sans changer le script.

Je pense qu'avec l'Orange Pi Zero et aussi le Raspberry Pi Zero, il est possible de supposer que le périphérique d'événement associé au pilote IR est /dev/input/event0 si aucun autre périphérique n'est connecté. Ce n'est pas le cas sur mon ordinateur de bureau où le lecteur IR est connecté par un interface USB ainsi que le clavier, la souris et d'autres appareils. J'ai vu qu'il fallait vérifier, car selon les circonstances, le périphérique d'entrée pouvait être input1 ou input2. Voici comment je règle ce problème en utilisant l'identificateur USB de récepteur IR.

irDriver = '0471:060c' devices = [evdev.InputDevice(path) for path in evdev.list_devices()] for device in devices: #print('path: ',device.path, ', name: ', device.name, ', phys: ', device.phys) if irDriver in device.name: devpath = device.path device = evdev.InputDevice(devpath) if verbose: print('Using input device: ', devpath)

L'identificateur USB du lecteur IR devrait aussi être un élément du fichier de configuration. Dans l'Orange Pi Zero, on peut utiliser le nom du module.

irDriver = 'sunxi-ir'

On peut voir que le programme contient trois fonctions qui ne font rien: showDeviceIndex, showStartRequest et showEndRequest. J'aimerais rajouter un écran pour afficher le nombre saisi ainsi qu'une indication qu'une requête HTML a été acheminée. Cependant, le script fonctionne très bien avec la télécommande Hauppauge et ce ne sera pas nécessaire de confirmer la réception des signaux IR. Si je devais utiliser la télécommande KEYES, le besoin d'une rétroaction serait beaucoup plus grand.

Conclusion toc

J'envisageais de continuer avec l'utilisation de LIRC. Grâce au programme irexec, utilisé en conjonction avec le service lircd, il est possible de prendre en charge une télécommande IR sans créer un programme Python pour autant que ce que l'on désire faire n'est pas trop complexe. Cette discussion est reportée à plus tard, j'ai rencontré un problème très vexant. La belle solution que j'avais trouvée n'a pas survécu à un redémarrage (ceci étant dit, IR Remote for Domoticz using irexec est toujours valable. Entre temps j'ai plusieurs projets en chantier bien plus amusants : le VPN WireGuard sur le Raspberry Pi (ça semble bien fonctionner), la reprogrammation de de prise connectées WiFi de type Tuya (une réussite pour 3 des 4 achetées), remplacement d'une télécommande IR en piteux état et introuvable (ça fonctionne avec ir-ctl de v4l-utils...

<-Télécommandes à infrarouge sur l'Orange Pi Zero; le matériel