Raspberry Pi 3

Contrôler son Raspberry Pi grâce à la télécommande

Alexandre Laurent

Les utilisateurs du media center Kodi l'ont remarqué : il est possible de manipuler l'interface à la télécommande. Voyons, dans ce tutoriel, comment recevoir les informations de la télécommande afin d'interagir avec notre Raspberry Pi.

Commentez

- Titre : Raspberry Pi 3

- Auteur : Alexandre Laurent

- Parution : 26 janvier 2017

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2017 Alexandre Laurent. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- I-A. Support

- I-B. Configuration

- II. Démarrage

- II-A. Faire un scan des périphériques CEC

- II-B. Allumer/Mettre en veille la télévision

- II-C. Obtenir la liste des commandes disponibles

- III. Contrôler le Raspberry Pi avec la télécommande

- III-A. À partir de BASH

- III-B. À partir de libCEC

- III-B-1. libCEC3

- III-B-1-a. En C

- III-B-2. LibCEC4

- III-B-2-a. Installation

- III-B-2-b. En C

- III-B-2-c. En Python

- IV. Conclusion

- V. Remerciements

I - Introduction

Le High-Definition Multimedia Interface (HDMI) est une norme définissant une interface audio et vidéo entièrement numérique. Mais en plus de faire transiter des flux, une seconde norme, qui est optionnelle, permet la communication de commandes de contrôle entre les périphériques connectés. Celle-ci s'appelle Consumer Electronics Control (CEC) et cet article explique comment contrôler le Raspberry Pi grâce aux signaux envoyés à la télévision par la télécommande.

C'est grâce à la norme CEC, que nous pouvons contrôler Kodi avec la télécommande.

I-A - Support

Tous les appareils ne supportent par la norme CEC. Cette page énumère les constructeurs supportant la norme et quelles sont les commandes supportées.

Pour ne pas aider, chaque constructeur utilise un nom différent pour appeler leur propre implémentation de la norme(1) :

Marque

Nom de l'implémentation

AOC

E-Link

Funai, Sylvania, Emerson, Magnavox, Philips

Fun-Link

Insignia

INlink

ITT

T-Link

Hitachi

HDMI-CEC

LG

SimpLink

Mitsubishi

NetCommand for HDMI

Onkyo

Remote Interactive over HDMI (RIHD)

Panasonic

EZ-Sync

Philips

EasyLink

Pioneer

Kuro Link

Samsung

Anynet+

Sharp

Aquos Link

Sony

BRAVIA Link, BRAVIA Sync, Control for HDMI

Toshiba

CE-Link, Regza Link

Runco International

RuncoLink

I-B - Configuration

La configuration utilisée à travers le tutoriel est la suivante :

- une télévision connectée au Raspberry Pi à l'aide d'un câble HDMI ;

- une télécommande permettant de contrôler la télévision.

Tous les câbles HDMI ne supportent pas la norme CEC.

II - Démarrage

Les distributions Linux ont accès aux fonctionnalités de la norme CEC à travers la bibliothèque libCEC. Celle-ci ne suffit pas pour facilement accéder aux fonctionnalités liées à la norme CEC. GNU/Linux intègre aussi un programme utilitaire qui écoutera et affichera les données CEC provenant du câble HDMI :

sudo aptitude install cec-utils

II-A - Faire un scan des périphériques CEC

Avant toute chose, détectons les périphériques CEC connectés :

echo "scan" | cec-client -s -d 1

Dans la documentation de cec-client, nous pouvons découvrir que l'option -s permet d'exécuter une commande unique (et après, le programme quitte) et -d permet de définir le niveau de journalisation.

La commande retourne :

opening a connection to the CEC adapter...
requesting CEC bus information ...
CEC bus information
===================
device #0: TV
address:       0.0.0.0
active source: no
vendor:        Samsung
osd string:    TV
CEC version:   unknown
power status:  standby
language:      fra

device #1: Recorder 1
address:       2.0.0.0
active source: no
vendor:        Pulse Eight
osd string:    CECTester
CEC version:   1.4
power status:  on
language:      eng

currently active source: unknown (-1)

Dans la sortie de cec-client, nous observons deux périphériques alors que nous n'avons qu'une seule télévision. Cela est dû au fait que pour interagir avec les périphériques CEC, votre périphérique doit s'inscrire à ce réseau. C'est ce que fait ici le client, en initialisant un périphérique « CECTester ».

II-B - Allumer/Mettre en veille la télévision

Pour commencer, allumons la télévision grâce à cec-utils. Pour cela, il faut envoyer un signal « on 0 ».

echo "on 0" | cec-client -s

De la même façon, nous pouvons remettre la télévision en veille :

echo "standby 0" | cec-client -s

Sachant que seule la télévision est connectée au Raspberry Pi, la télévision possède donc l'identifiant 0 au sein de l'outil cec-client.

II-C - Obtenir la liste des commandes disponibles

Précédemment, nous avons utilisé les commandes scan, on et standby. Toutefois, il n'est pas possible de deviner ces commandes : cec-client possède un code spécifique pour obtenir les commandes disponibles :

echo h | cec-client -s -d 1

III - Contrôler le Raspberry Pi avec la télécommande

Avant de commencer, nous pouvons vérifier que les appuis de touches de la télécommande sont bien envoyés au Raspberry Pi avec la commande :

cec-client

Pour chaque appui, nous pouvons lire :

TRAFFIC: [            7267]    >> 01:44:04
DEBUG:   [            7267]    key pressed: right (4)
DEBUG:   [            7267]    >> TV (0) -> Recorder 1 (1): user control pressed (44)
TRAFFIC: [            7375]    >> 01:8b:04
DEBUG:   [            7375]    key released: right (4)
DEBUG:   [            7375]    >> TV (0) -> Recorder 1 (1): vendor remote button up (8B)

Si lorsque vous appuyez sur les flèches de votre télécommande, rien ne se passe dans la sortie de cec-client, allez dans les paramètres de votre télévision afin d'activer CEC, ou encore de lancer une recherche des périphériques connectées à travers CEC, et d'indiquer à la télévision d'utiliser le périphérique « CECTester ».

Si cec-client ne réagit toujours pas à la télécommande, tapez « as » directement dans la sortie de cec-client pour passer la télévision en source active.

III-A - À partir de BASH

Vous pouvez rediriger la sortie de cec-client dans un fichier. Ensuite, il suffit de lire ce fichier avec un script pour savoir sur quelle touche l'utilisateur a appuyé :

cec-test.sh

echo "" > ${1}
while true
do

if  grep -q "key pressed: left" ${1}
then
echo "Vous avez appuyé sur la flèche gauche"
fi
if grep -q "key pressed: right" ${1}
then
echo "Vous avez appuyé sur la flèche droite - BYE"
exit 0
fi
echo "" > ${1}
sleep 1s

done

Voici comment lancer cec-client pour rediriger la sortie dans un fichier :

cec-client -f /tmp/cec

Ensuite, vous pouvez lancer le script, qui ira lire dans le fichier écrit par cec-client :

./cec-test.sh /tmp/cec

Et retournera ce qui suit, après avoir appuyé trois fois à gauche et une fois à droite :

Vous avez appuyé sur la flèche gauche
Vous avez appuyé sur la flèche gauche
Vous avez appuyé sur la flèche gauche
Vous avez appuyé sur la flèche droite - BYE

III-B - À partir de libCEC

Afin d'écrire un programme communiquant dans la norme CEC, vous devez installer la bibliothèque libCEC :

apt-get install libcec-dev

Dans la suite des exemples, l'affichage du journal complet de la bibliothèque a été commenté. Décommenttez la ligne relative aux logs afin d'obtenir de meilleures informations sur la connexion entre le Raspberry Pi et la télévision.

III-B-1 - libCEC3

Raspbian, à travers les paquets de la distribution, propose la version 3 de la bibliothèque libCEC.

III-B-1-a - En C

Le programme suivant vous permettra de détecter les appuis sur les touches de la télécommande :

test-cecc.c

#include <libcec/cecc.h>

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

static int g_bExit=0;
// Fonction appelée lorsque le programme reçoit un signal SIGINT
// => provoque l'arrêt de la boucle principale du main()
static void sighandler(int iSignal)
{
printf("Capture du signal : %d - sortie\n", iSignal);
g_bExit = 1;
}

// Fonction pour afficher les messages de journalisation de libcec
static int cb_cec_log_message(void* lib, const cec_log_message message)
{
(void)lib;

const char* strLevel;
switch (message.level)
{
case CEC_LOG_ERROR:
strLevel = "ERROR:   ";
break;
case CEC_LOG_WARNING:
strLevel = "WARNING: ";
break;
case CEC_LOG_NOTICE:
strLevel = "NOTICE:  ";
break;
case CEC_LOG_TRAFFIC:
strLevel = "TRAFFIC: ";
break;
case CEC_LOG_DEBUG:
strLevel = "DEBUG:   ";
break;
default:
break;
}

printf("%s[%lld]\t%s\n", strLevel, message.time, message.message);
return 0;
}

// Fonction appelée à chaque appui sur une touche de la télécommande
static int keyPressHandler(void* lib, const cec_keypress key)
{
(void)lib;

printf("Appui touche détecté : %d pendant %d\n",key.keycode,key.duration);

return 0;
}

int main()
{
// Installation de notre gestionnaire de signaux
if (signal(SIGINT, sighandler) == SIG_ERR)
{
printf("Impossible de placer le gestionnaire de signaux\n");
return -1;
}

libcec_configuration g_config;
libcec_connection_t g_iface;
char strPort[10];

// Configuration des callbacks
ICECCallbacks g_callbacks;
// g_callbacks.CBCecLogMessage = &cb_cec_log_message;
g_callbacks.CBCecKeyPress = &keyPressHandler;

// Configuration de notre instance de libcec
libcec_clear_configuration(&g_config);
g_config.clientVersion        = LIBCEC_VERSION_CURRENT;
g_config.bActivateSource      = 0;
g_config.callbacks            = &g_callbacks;
snprintf(g_config.strDeviceName, sizeof(g_config.strDeviceName), "libCEC test");
g_config.deviceTypes.types[0] = CEC_DEVICE_TYPE_RECORDING_DEVICE;

// Initialisation
g_iface = libcec_initialise(&g_config);
if (g_iface == NULL)
{
printf("Impossible d'initialiser libCEC\n");
return -1;
}

// Appel obligatoire après l'initialisation de la bibliothèque pour initialiser la machine hôte
libcec_init_video_standalone(g_iface);

// Recherche du périphérique de connexion
cec_adapter devices[10];
int8_t iDevicesFound;
printf("Autodétection périphérique : ");

iDevicesFound = libcec_find_adapters(g_iface, devices, sizeof(devices) / sizeof(devices), NULL);
if (iDevicesFound <= 0)
{
printf("ECHEC\n");
libcec_destroy(g_iface);
return 1;
}
else
{
printf("\n Chemin :    %s\n Port :      %s\n\n", devices[0].path, devices[0].comm);
strcpy(strPort, devices[0].comm);
}

// Ouverture de la connexion
printf("ouverture d'une connexion à l'adaptateur CEC...\n");
if (!libcec_open(g_iface, strPort, 5000))
{
printf("impossible d'ouvrir le périphérique avec le port %s\n", strPort);
libcec_destroy(g_iface);
return 1;
}

while (!g_bExit)
{
usleep(50000);
}

// Nettoyage (fermeture de la bibliothèque)
libcec_destroy(g_iface);

return 0;
}

Vous pouvez le compiler avec la commande :

gcc test-cecc.c -Wall -Wextra -o ./test-cecc -lcec3

Et vous obtiendrez une sortie de la sorte :

Autodétection périphérique :
Chemin :    Raspberry Pi
Port :      RPI

ouverture d'une connexion à l'adaptateur CEC...
Appui touche détecté : 3 pendant 0
Appui touche détecté : 3 pendant 107
Appui touche détecté : 3 pendant 0
Appui touche détecté : 3 pendant 120
Appui touche détecté : 4 pendant 0
Appui touche détecté : 4 pendant 111
^CCapture du signal : 2 - sortie

(Ici, j'ai appuyé trois fois sur la flèche gauche (3), et une fois sur la flèche droite (4) de la télécommande.)

III-B-2 - LibCEC4

Depuis le 26 octobre 2016, la version 4 de la bibliothèque libCEC est disponible.

Cette nouvelle version apporte des améliorations sur l'implémentation de la bibliothèque en C tout en corrigeant des bogues dont deux reproductibles avec le Raspberry Pi 3.

Tant que la version 4 n'est pas disponible à travers les paquets de la distribution utilisée, il faudra la compiler à partir du code source pour l'installer. De plus, le fait de compiler la bibliothèque vous permet de l'utiliser avec Python.

III-B-2-a - Installation

La procédure est détaillée sur GitHub :

sudo apt-get update
sudo apt-get install cmake libudev-dev libxrandr-dev python-dev swig
cd
git clone https://github.com/Pulse-Eight/platform.git
mkdir platform/build
cd platform/build
cmake ..
make
sudo make install
cd
git clone https://github.com/Pulse-Eight/libcec.git
mkdir libcec/build
cd libcec/build
cmake -DRPI_INCLUDE_DIR=/opt/vc/include -DRPI_LIB_DIR=/opt/vc/lib ..
make -j4
sudo make install
sudo ldconfig

III-B-2-b - En C

Voici le même programme C que pour la version 3, converti pour la nouvelle version de libCEC :

#include <libcec/cecc.h>

#include <signal.h>
#include <stdio.h>
#include <unistd.h>

static int g_bExit=0;
// Fonction appelée lorsque le programme reçoit un signal SIGINT
// => provoque l'arrêt de la boucle principale du main()
static void sighandler(int iSignal)
{
printf("Capture du signal : %d - sortie\n", iSignal);
g_bExit = 1;
}

// Fonction pour afficher les messages de journalisation de libcec
static void cb_cec_log_message(void* lib, const cec_log_message* message)
{
(void)lib;

const char* strLevel;
switch (message->level)
{
case CEC_LOG_ERROR:
strLevel = "ERROR:   ";
break;
case CEC_LOG_WARNING:
strLevel = "WARNING: ";
break;
case CEC_LOG_NOTICE:
strLevel = "NOTICE:  ";
break;
case CEC_LOG_TRAFFIC:
strLevel = "TRAFFIC: ";
break;
case CEC_LOG_DEBUG:
strLevel = "DEBUG:   ";
break;
default:
break;
}

printf("%s[%lld]\t%s\n", strLevel, message->time, message->message);
}

// Fonction appelée à chaque appui sur une touche de la télécommande
static void keyPressHandler(void* lib, const cec_keypress* key)
{
(void)lib;

printf("Appui touche détecté : %d pendant %d\n",key->keycode,key->duration);
}

// Callbacks (bien faire attention que les callbacks non utilisés soient à NULL)
static ICECCallbacks        g_callbacks = {
//    .logMessage           = cb_cec_log_message,
.logMessage           = NULL,
.keyPress             = keyPressHandler,
.commandReceived      = NULL,
.configurationChanged = NULL,
.alert                = NULL,
.menuStateChanged     = NULL,
.sourceActivated      = NULL
};

int main()
{
// Installation de notre gestionnaire de signaux
if (signal(SIGINT, sighandler) == SIG_ERR)
{
printf("Impossible de placer le gestionnaire de signaux\n");
return -1;
}

libcec_configuration g_config;
libcec_connection_t g_iface;
char strPort[10];

// Configuration de notre instance de libcec
libcec_clear_configuration(&g_config);
g_config.clientVersion        = LIBCEC_VERSION_CURRENT;
g_config.bActivateSource      = 0;
g_config.callbacks            = &g_callbacks;
snprintf(g_config.strDeviceName, sizeof(g_config.strDeviceName), "libCEC4");
g_config.deviceTypes.types[0] = CEC_DEVICE_TYPE_RECORDING_DEVICE;

// Initialisation
g_iface = libcec_initialise(&g_config);
if (g_iface == NULL)
{
printf("Impossible d'initialiser libCEC\n");
return -1;
}

// Appel obligatoire après l'initialisation de la bibliothèque pour initialiser la machine hôte
libcec_init_video_standalone(g_iface);

// Recherche du périphérique de connexion
cec_adapter devices[10];
int8_t iDevicesFound;
printf("Autodétection périphérique : ");

iDevicesFound = libcec_find_adapters(g_iface, devices, sizeof(devices) / sizeof(devices), NULL);
if (iDevicesFound <= 0)
{
printf("ECHEC\n");
libcec_destroy(g_iface);
return 1;
}
else
{
printf("\n Chemin:     %s\n Port:       %s\n\n", devices[0].path, devices[0].comm);
strcpy(strPort, devices[0].comm);
}

// Ouverture de la connexion
printf("ouverture d'une connexion à l'adaptateur CEC...\n");
if (!libcec_open(g_iface, strPort, 5000))
{
printf("impossible d'ouvrir le périphérique avec le port %s\n", strPort);
libcec_destroy(g_iface);
return 1;
}

while (!g_bExit)
{
usleep(50000);
}

// Nettoyage (fermeture de la bibliothèque)
libcec_destroy(g_iface);

return 0;
}

Le code ci-dessus compilera avec la commande suivante :

gcc test-cecc.c -Wall -Wextra -o ./test-cecc -lcec4

Vous pouvez obtenir la sortie suivante :

Autodétection périphérique :
Chemin:     Raspberry Pi
Port:       RPI

ouverture d'une connexion à l'adaptateur CEC...
Appui touche détecté : 3 pendant 0
Appui touche détecté : 3 pendant 220
Appui touche détecté : 0 pendant 0
Appui touche détecté : 0 pendant 108
Appui touche détecté : 33 pendant 0
Appui touche détecté : 33 pendant 110

(Ici, j'ai appuyé sur la flèche gauche (3), la touche OK (0) et la touche 1 (33) de la télécommande.)

III-B-2-c - En Python

Grâce à la compilation de la bibliothèque, vous avez aussi obtenu le wrapper Python.

Voici un petit programme pour tester et récupérer les appuis sur les touches de la télécommande :

#!/usr/bin/python3

import cec

def LogCallback(level, time, message):
if level == cec.CEC_LOG_ERROR:
levelstr = "ERROR:   "
elif level == cec.CEC_LOG_WARNING:
levelstr = "WARNING: "
elif level == cec.CEC_LOG_NOTICE:
levelstr = "NOTICE:  "
elif level == cec.CEC_LOG_TRAFFIC:
levelstr = "TRAFFIC: "
elif level == cec.CEC_LOG_DEBUG:
levelstr = "DEBUG:   "

print(levelstr + "[" + str(time) + "]     " + message)
return 0

def keyPressCallback(key, duration):
print("[key pressed] " + str(key))
return 0

# Configuration de notre instance libcec
cecconfig = cec.libcec_configuration()
cecconfig.strDeviceName = "libCEC"
cecconfig.bActivateSource = 0
cecconfig.deviceTypes.Add(cec.CEC_DEVICE_TYPE_RECORDING_DEVICE)
cecconfig.clientVersion = cec.LIBCEC_VERSION_CURRENT
# cecconfig.SetLogCallback(LogCallback)
cecconfig.SetKeyPressCallback(keyPressCallback)

# Lancement et ouverture de notre périphérique
lib = cec.ICECAdapter.Create(cecconfig)
adapter = lib.DetectAdapters()[0].strComName
lib.Open(adapter)

while True:
command = input("...").lower()
if command == 'q':
break

Vous obtiendrez une sortie comme suit :

[key pressed] 3
[key pressed] 3
[key pressed] 2
[key pressed] 2
[key pressed] 68
[key pressed] 68

(Ici, j'ai appuyé sur la flèche gauche (3), la touche bas (2) et la touche « Lecture » (68) de la télécommande.)

On remarque deux lignes par touche appuyée : la première indique l'appui, la seconde le relâchement.

IV - Conclusion

Grâce à libCEC et cec-utils nous avons pu découvrir comment lire les événements envoyés à la télévision par la télécommande. Avec ces bases, on peut maintenant mettre en place une infinité de programmes se servant de la télécommande comme interface d'entrées utilisateur.

Par exemple, vous pouvez réutiliser le script BASH ci-dessus en l'associant avec xdotool comme suit, afin de déplacer le curseur de la souris à l'aide de la télécommande :

#!/bin/bash

echo "" > ${1}
while true
do

if  grep -q "key pressed: left" ${1}
then
xdotool mousemove_relative -- -10 0
fi
if grep -q "key pressed: right" ${1}
then
xdotool mousemove_relative 10 0
fi
if grep -q "key pressed: down" ${1}
then
xdotool mousemove_relative 0 10
fi
if grep -q "key pressed: up" ${1}
then
xdotool mousemove_relative -- 0 -10
fi
if grep -q "key pressed: select" ${1}
then
xdotool click 1
fi

echo "" > ${1}
sleep 1s

done

V - Remerciements

Cet article n'aurait pas pu voir le jour sans le support de la Raspberry Pi Foundation. En effet, la fondation m'a envoyé un exemplaire du Raspberry Pi 3 : modèle B afin de permettre l'écriture de ce document.
Developpez.com et moi-même tenons à remercier la Raspberry Pi Foundation.

Merci aussi aux relecteurs : Franck Talbart et zoom61, ainsi qu'au relecteur orthographique : Maxy35.

(1)Source : Wikpedia