Club robotique de Sophia-Antipolis

Accueil > POBOTpedia > Ordinateurs embarqués (SBC) > La carte Raspberry PI > Installation et configuration de la R-Pi > D-Bus sur la RasPi

D-Bus sur la RasPi

jeudi 3 janvier 2013, par Eric P.

Attention, cet article est un pavé !! D’une part parce que je suis souvent bavard lorsque je prends la plume, mais aussi parce que le sujet mérite à mon avis un développement sérieux, afin que le lecteur puisse comprendre les tenants et les aboutissants de la démarche, en plus de trouver les éléments techniques et références nécessaires pour reproduire l’approche.

Quelques mots d’introduction avant de commencer :
IPC : késako ?
IPC = Inter-Process Communication.
D-Bus : késako ?
D-Bus est quelque chose qui se tapit dans la plupart des distros Linux depuis maintenant un certain temps, y compris dans celle de votre framboise préférée, et qui permet de faire de l’IPC

 Pourquoi je vous en parle aujourd’hui ?

Parce que cela permet de construire des applications modulaires, constituées d’un ensemble d’exécutables, de scripts,... qui assurent chacun une fonction bien précise, et qui communiquent entre eux pour réaliser les fonctions demandées. En fait, ce n’est rien d’autre que la philosophie qui est la base d’Unix et qui a été reprise par toutes ses déclinaisons, dont Linux, à savoir : ne pas concevoir un système comme un énorme logiciel qui fait tout (et souvent mal...) mais comme une collection d’outils réalisant chacun une tâche précise et unique, mais la faisant bien.

Pour nous autres les bricoleurs de robots, on voit assez bien ce que cela peut nous apporter. Un robot, ne l’oublions pas, ce sont des capteurs, des actionneurs et une logique de contrôle. Tout cela est éminemment modulable mais on retrouve des éléments récurrents, et donc fortement réutilisables. Donc, de la même manière qu’une boite de LEGO permet de faire plusieurs modèles différents avec les mêmes briques de base, faisons la même chose avec le logiciel. En ingénierie logicielle, on appelle cela la construction d’application par assemblage de composants.

 Soit, me direz-vous, mais les bibliothèques sont faites pour cela

Eh bien contrairement aux apparences, ce n’est pas le cas. Une bibliothèque permet de réutiliser des morceaux d’algorithme mais ne fournit pas de composant au sens où on l’entend ici. Une bibliothèque permettra d’implémenter les composants, mais pas plus.

D’autre part, une bibliothèque est liée à un langage donné, et même si on peut marier des choses écrites dans des langages différents, cela reste limité. Cela s’appelle un couplage par l’API, qui s’il apporte des avantages indéniables en termes de performances et de vérification de typage à la compilation, vient de ce fait avec les inconvénients de ses avantages : un couplage fort et donc peu souple. Sans parler de pouvoir remplacer un module par un autre équivalent sans devoir à minima refaire une édition de liens. Les Javaistes vont me rire au nez en disant qu’il leur suffit de remplacer un fichier jar par un autre du moment que l’interface reste la même, mais là encore cela ne fonctionnera que tant qu’on se restreint à un monde mono-langage.

Ce que nous voulons faire ici c’est construire notre application de contrôle du robot en assemblant des composants spécialisés, éventuellement écrits dans des langages différents de manière à tirer parti des forces des langages utilisés et des compétences des auteurs respectifs. Et qui dit assembler, dit faire communiquer afin de pouvoir collaborer.

Nous réalisons ici un couplage par les données car ce ne sont plus vraiment les API qui régissent la communication mais les données échangées. Ne vous laissez en effet pas abuser par le RPC mentionné plus haut : ce qui transite sur le bus ce sont des paquets de données structurées, les API manipulées au niveau du RPC n’étant que l’interface qui est implémentée par le binding pour le langage utilisé, et ce à titre de simplification de la mise en oeuvre des mécanismes propres à D-Bus. Pour vous en convaincre, amusez-vous à lire la documentation de la couche low-level (http://dbus.freedesktop.org/doc/api...), que personne n’utilise en dehors des implémenteurs de bindings (il y a d’ailleurs un avertissement en ce sens, de la main même des auteurs de cette documentation).

 Tout cela est bien joli, mais n’est-ce pas de l’onanisme cérébral ?

Oui et non.

Oui, car on peut arriver au même résultat de manière classique, si on sacrifie l’objectif de modularité et ré-utilisabilité.

Non, car cela résultera la plupart du temps en un programme de type plat-de-spaghetti, difficilement capitalisable et mono-langage. Tant qu’il n’y a qu’une seule personne impliquée pourquoi pas, mais notre optique au sein de POBOT est de partager, et donc de faire en sorte que ce que nous produisons ne soit pas à usage unique pour une seule personne (sinon autant rester chacun dans son coin).

 Admettons, mais quel rapport avec les robots et la RasPi ?

Il se trouve que je suis présentement [1] en train de travailler sur le contrôleur de notre robot pour la Coupe de France 2013.

Histoire de se faire un peu plaisir, un choix d’architecture distribuée a été retenu, avec des cartes mBed dédiées à des fonctions critiques et bien précises comme le contrôle des moteurs et l’odométrie, et une RasPi comme superviseur. Ce superviseur a pour charge de faire tourner tout ce qui contrôle le déroulement du match (et des futures démos), et il va pour cela communiquer avec un certain nombre de ressources matérielles :
- des capteurs
- les contrôleurs de moteur
- les servos numériques
- l’interface utilisateur

En plus de cela, il doit aussi gérer les différentes stratégies prévues, voire des algorithmiques avancés, et le tout de manière interchangeable et réutilisable, car une fois de plus, ce robot aura une vie après la Coupe [2] et le but est donc qu’il puisse être facilement adaptable au niveau logiciel.

Donc pas question de tout faire d’un seul bloc et écrit en C/C++ par exemple.

L’approche retenue est de tout décomposer en modules spécialisés, chacun étant écrit dans le langage le plus approprié [3]. Ces modules sont lancés en fonction des besoins en tant que process indépendants, et communiquent entre eux via D-Bus.

A titre d’exemple, nous disposons des modules suivants en date de rédaction :
- la supervision du match
- la gestion de l’interface utilisateur embarquée (LCD + keypad)

C’est peu me direz-vous, mais cela ne fait même pas une semaine que j’ai mis ma RasPi sous tension pour la première fois, ce qui ne m’a pas empêché de commettre déjà un autre article et une amorce de toolbox sur GitHub ;)

Un dernier point que j’ai failli oublier : l’approche totalement incrémentale. Il est en effet possible de simuler un module non encore disponible lors des tests tout simplement en invoquant les interfaces au travers d’outils en ligne de commande comme dbus-send et en monitorant les échanges avec dbus-monitor [4]. Tout cela est détaillé plus loin dans cet article.

 Et si nous parlions de D-Bus...

Il serait temps effectivement.

Je ne vais ici pas répéter ce qui est déjà tartiné à maints endroits sur le Web, mais simplement rassembler les pointeurs vers les ressources utiles et surtout ajouter deux ou trois compléments personnels qui pourront aider à capter rapidement la chose.

En effet, un des gros problèmes de D-Bus est sa documentation. Elle n’est pas inexistante, mais elle souffre de lacunes et de défauts, qu’on finit par combler au fur à mesure des expérimentations et recherches documentaires, mais cela est un processus chronophage à l’extrême. De plus si vous faites un coup de Google, vous allez trouver une pléiade de pages sans aucun intérêt, dans lesquels les auteurs se contentent de ressasser tout le temps les mêmes exemples sans aucune valeur pédagogique et surtout sans explication réellement utile au néophyte. Ca ressemble plus à une satisfaction d’égo qu’on pourrait résumer la plupart du temps par : "regardez comme je suis bon et ce que j’ai réussi à faire : hello world en D-Bus".

Pour faire simple, D-Bus a ses origines dans Gnome, au départ pour permettre aux différents constituants du desktop et aux applis de communiquer entre eux. Pour reprendre la définition proposé sur son site de référence (http://www.freedesktop.org/wiki/Sof...) :


D-Bus is a message bus system, a simple way for applications to talk to one another. In addition to interprocess communication, D-Bus helps coordinate process lifecycle ; it makes it simple and reliable to code a "single instance" application or daemon, and to launch applications and daemons on demand when their services are needed.


En fait (et ce n’est pas vraiment explicite en lisant le texte ci-dessus), D-Bus permet les choses suivantes :
- communiquer entre applications, pouvant être écrites en langages différents, par simple appel de méthodes. Ceci est appelé RPC [5] dans d’autres domaines.
- communiquer entre applications par broadcast de signaux
- gérer automatiquement (ie c’est D-Bus qui s’en charge) le cycle de vie (traduire : le lancement et l’arrêt) des programmes fournissant les services applicatifs, évitant ainsi de devoir gérer explicitement leur cycle de vie
- fournir un mécanisme de sécurité intégré permettant de contrôler qui a le droit d’accéder à quoi, et ce de manière très fine
- last but not least, et cela n’est écrit explicitement nulle part dans toutes les docs que j’ai pu lire [6], même sur le site de référence : D-Bus "traverse" les réseaux. Autrement dit, D-Bus n’est pas cantonné à une machine, mais il est possible de "raccorder" entre eux des bus tournant sur des machines distinctes, et ce sans aucun hack, mais par simple paramétrage ad-hoc de la configuration des deamons.

Ceci étant, D-Bus est très largement sorti du contexte graphique du desktop puisqu’utilisé notamment au niveau des couches HAL pour par exemple notifier de l’ajout ou de la suppression d’un device (un bidule USB par exemple). Idem pour les changements d’état des connexions réseau. Et ceci n’est qu’un infime aperçu de l’étendue des utilisations de D-Bus au sein des couches systèmes des distributions Linux actuelles.

Un des points qui va être abordé dans ce qui suit est l’utilisation de D-Bus par une application, mais hors d’un contexte d’environnement graphique de type session X, car il requiert de connaître les spécificités d’un tel contexte et la manière donc D-Bus le gère (très bien d’ailleurs). Pourquoi en parler : tout simplement parce que tous les exemples que vous allez pouvoir trouver (bien souvent écrits par les mêmes individus que ceux dont je parlais quelques paragraphes plus haut) se cantonnent à piloter une des applications du desktop (le lecteur multi-média) pour leur démo. Vous conviendrez que l’intérêt est d’une part limité, mais d’autre part, ce faisant on passe à côté de quelques "détails" importants pour l’utilisation qui nous intéresse ici.

 La documentation

Ci-après une liste de liens vers les documentations qui m’ont été le plus utiles, et surtout qui présentent une valeur ajoutée intrinsèque :

Documentation de référence :

- http://www.freedesktop.org/wiki/Sof...
- http://packages.python.org/txdbus/d...
- http://www.freedesktop.org/wiki/Int...
- http://www.freedesktop.org/wiki/Sof...

J’attire votre attention sur le dernier lien de cette liste, car il permet de faire un parallèle entre les concepts de D-Bus et d’autres concepts avec lesquels vous êtes déjà familiers. En effet D-Bus introduit des notions qu’il est facile de confondre avec d’autres (certains choix de termes sont en effet discutables, et nous en verrons un bon exemple un peu plus loin), et ce court document a le mérite de dissiper un certain nombre de doutes et de zones d’ombre.

Tutoriels :

- http://dbus.freedesktop.org/doc/dbu...
- http://dbus.freedesktop.org/doc/dbu...
- http://yoannsculo.developpez.com/tu...

Je vous suggère de commencer par lire ce tout dernier tutoriel, car je l’ai trouvé très clair, et il est de plus en Français, ce qui pourra simplifier la vie à certains d’entre vous.

 Les points clés à retenir

Tout d’abord, malgré le terme de bus, nous sommes ici en présence de communications point à point, basées sur des sockets de manière interne. La vision de bus est apporté par l’existence du hub qui gère les échanges, ce qui est très bien illustré par le schéma suivant (emprunté au tutoriel de Yoann Sculo) :

PNG - 46.7 ko
Représentation graphique de D-Bus

Cette vision est très importante car elle permet de dissiper une ambiguïté de terminologie générée par le terme busname.

En effet, il est probable que cela soit la conséquence de ma grande neuneuitude, mais j’avais au départ interprété busname comme name of the bus. Fatal error, à l’origine d’un tas de grattages de citron et de migraines. Qui plus est cette interprétation (erronée comme vous vous en doutez) est entretenue par le nom de certaines méthodes dans les API. Comme quoi on ne répétera jamais assez que le choix des identificateurs est fondamental lorsqu’on écrit du code qui a vocation d’être réutilisé par d’autres, et que c’est une erreur catastrophique et une source colossale de perte de productivité que de considérer que c’est secondaire et d’utiliser des identificateurs alakon ou pire des traductions anglaises approximatives porteuses de contre-sens [7].

En fait, busname doit être interprété comme name ON the bus, ce qui d’ailleurs transparaît à quelques rares endroits lorsqu’on voit mentionner l’appellation alternative qui est well-known name. En fait, comme mentionné dans la documentation qui présente les analogies, le busname est au service qui est connecté à un bus ce que le hostname que vous mettez dans le fichier /etc/hosts est à la machine concernée, ou bien le nom de serveur fourni par un DNS à son adresse IP sur Internet. A la différence cependant du fichier hosts ou du DNS, ce nom n’est pas déclaré dans un emplacement tiers, mais est fourni par le service lui-même lorsqu’il se connecte sur le bus au moment de son initialisation, et effectue ce qu’on appelle un claim pour ce nom symbolique.

Eh bien en fait, c’est tout. Armé de ces petites précisions et du reste des documentations indiquées, vous en avez assez pour vous lancer dans l’aventure.

 Passons à la RasPi et à Python maintenant

"Il serait temps" allez-vous dire certainement. Certes, mais comme en toute chose, il faut se garder d’une précipitation néfaste. Comme dans d’autres domaines, négliger les préliminaires est une erreur dommageable ;)

Ajouter D-Bus à la RasPi est inutile, pour la simple raison qu’il y est déjà. Tout au plus allons nous ajouter le binding Python pour ceux (comme moi) qui ont choisi ce langage pour leurs expérimentations.

Le site de référence freedesktop.org fournit les pointeurs vers différents bindings disponibles, dont dbus-python. Pourquoi avoir choisi celui là ? Pour le fait qu’il soit basé sur libdbus, qui est l’implémentation de référence de D-Bus. D’autre part, il est assez bien documenté, simple à mettre en oeuvre et fiable [8]. La dernière version packagée en date de rédaction est la 1.1.1 du 25/06/2012, mais si vous aimez le risque vous pouvez aussi travailler avec la version en cours, accessible sur le repository git. J’ai préféré assurer en me contentant de la version publiée.

Nous allons donc passer à son installation sur la RasPi, car il s’agit d’une distribution en sources, à compiler par conséquent via la trilogie habituelle ./configure, make, make install.

Comme je l’ai mentionné dans d’autres articles sur la RasPi, j’utilise la distribution Occidentalis v0.2 de Adafruit. Ceci étant, ses aménagements par rapport à la Rasbian standard n’ont aucune incidence ici. Si vous travaillez avec autre chose (Arch, Gentoo,...) il va falloir assumer car je ne connais pas particulièrement ces distribs, hormis de nom, et il n’est pas question ici de lancer yet-another-flame-war sur le thème "ma distro est meilleure que la tienne".

Première chose à faire, récupérer l’archive et la décompacter dans un répertoire de travail sur la RasPi.

Ensuite, on s’occupe de la configuration. Si vous lancez ./configure comme moi sur une RasPi neuve ou quasi-neuve, vous allez être gratifié du message d’erreur "No package ’dbus-glib-1’ found" au bout de quelques instants. C’est juste que vous n’avez que le runtime de glib, et pas ses headers et autres ressources nécessaires pour la compilation de code basé dessus. Cette omission est vite corrigée par :
$ sudo apt-get install libdbus-glib-1-dev

Tant qu’on est dans l’installation de dépendances, et afin de vous éviter un coïtus interruptus au moment où, tout excité, vous allez lancer votre premier programme D-Bus, installons aussi le binding Python pour gobject, qui fournit entre autre le mécanisme de boucle d’événements nécessaire à l’exécution de toute appli basée sur D-Bus. Cela se fait très simplement via :
$ sudo apt-get install python-gobject

On peut maintenant passer à la compilation en lançant tout simplement make. Tout doit normalement se dérouler sans aucun problème si votre configuration n’a pas été personnalisée de manière trop audacieuse (sinon, tant pis pour vous : "de grands pouvoirs entraînent de grandes responsabilités" [9]). Dernière étape, l’installation dans le système via sudo make install.

That’s all folks. Vous êtes parés.

 Utilisation pratique

Je vais ici mettre l’accent sur l’utilisation dans le cadre concret de la configuration cible, à savoir une RasPi tournant en mode headless, c’est à dire sans console et donc sans environnement X (pourquoi diable gaspiller des ressources avec X s’il n’y a personne pour regarder les zoulies fenêtres ?).

Les applications concernées seront donc lancées lors de l’init (ou du moins au minimum le bootstrap, si on se base sur l’activation automatique par D-Bus des services requis), c’est à dire en dehors de toute session utilisateur et sous le compte de root par conséquent.

Or le bus accessible librement, et utilisé dans tous les exemples à deux balles qu’on peut trouver sur le net, est le bus session (SessionBus) qui est créé dans le cadre du processus d’ouverture de la session X. Dans notre situation il n’existe donc pas et le seul bus disponible est le bus système (SystemBus), créé lui au tout début de l’init et qui est utilisé comme son nom l’indique par tous les composants du système qui ont besoin d’échanger de l’information ou de notifier des événements (cf ce qui a été dit en tout début de ce roman-fleuve).

Ceci est important, car le bus système est sécurisé à la manière d’un firewall digne de ce nom : tout est fermé par défaut et personne n’a le droit de rien. Par conséquent, pour que nos codes puissent se déclarer en tant que services accessibles via D-Bus et autoriser des tiers à utiliser leurs interfaces, il va falloir le configurer quelque part. Faute de quoi la première tentative va se solder par une erreur de type "Access denied". Je trouve cela très rassurant, surtout dans le cadre d’une machine "normale", c’est à dire amenée à être connectée à un réseau public. Ce n’est pas le cas ici, mais si la RasPi est utilisée pour autre chose qu’un robot déconnecté, ça prend toute son importance, d’autant que comme cela a été mentionné plus haut, il est possible de faire sortir un bus D-Bus de la machine et donc de le connecter au reste du réseau. Nous parlerons de ce point (la configuration des droits) en fin d’article, afin d’aborder en priorité ce qui est plus fun.

Dans un autre article j’ai présenté les expérimentations faites pour interfacer un LCD I2C avec la RasPi, ces manips ayant eu pour résultat un article, et surtout un module Python. Nous allons ici créer un service D-Bus permettant d’utiliser ce LCD et son keypad depuis n’importe quel autre process tournant sur la RasPi, et ce de manière totalement découplée. Les fonctions les plus courantes sont mises à disposition par ce canal, à savoir :
- effacement
- positionnement
- écriture de texte
- configuration du curseur
- gestion du rétro-éclairage
- interrogation du keypad

De plus, et afin d’exploiter toutes les possibilités de D-Bus mais aussi de simplifier l’écriture des clients de ce service, il inclut un mécanisme de notification asynchrone de l’appui des touches du keypad, basé sur l’émission de signaux.

 Le service LCD détaillé

Le code intégral du module ne sera pas distribué ici, car d’une part il est en cours de finalisation et d’autre part il sera mis sous GitHub dès que prêt à être diffusé. Nous allons juste en extraire les éléments les plus illustratifs des points importants.

Pour commencer, la classe qui implémente le service est dérivée de dbus.service.Object. Etant donné que c’est un service, elle doit déclarer le nom symbolique sous lequel ce service sera accessible, aka le fameux busname [10]. Cette déclaration se fait en instanciant la classe dbus.service.BusName, et en précisant bien entendu sur quel bus on veut se connecter (le bus système ici). Voyez cela comme une connexion, d’où le nom que je lui ai donné dans le code. Sur l’illustration en début d’article, c’est le nom que le hub associe à l’identification unique qu’il a définie pour le socket qui le relie au process du service.

Une fois la connexion disponible, on finalise l’initialisation de notre instance en appelant le constructeur hérité, à qui on fournit la connexion obtenue, et le path sous lequel l’objet qui implémente le service est identifié. En effet, un même process peut implémenter plusieurs services, accessibles au travers de la même connexion, d’où la nécessité de cette seconde identification. A noter les différences de syntaxes : le busname utilise une notation qualifié avec un "." comme séparateur, alors que l’object path utilise des "/" (d’où son nom). Il n’y a aucune obligation à utiliser les mêmes membres dans les deux comme je l’ai fait ici, l’object path n’ayant d’existence qu’au sein du namespace défini par le busname. J’aurais pu me contenter de "/service" par exemple pour l’object path. Disons que je me lui laissé aller à reproduire les exemples des documents trouvés sur le Web.

On retrouve tout cela dans le code du constructeur de la classe :

BUSNAME = 'org.pobot.rob.Console'
OBJPATH = '/org/pobot/rob/Console/object'
class LcdConsole(dbus.service.Object):
    def __init__(self):
        # [ code d'initialisation du LCD supprimé pour simplifier l'exemple]
        # obtention d'une connexion au bus système
        connection = dbus.service.BusName(BUSNAME, bus=dbus.SystemBus())
        # initialisation de l'objet service
        dbus.service.Object.__init__(self, connection, OBJPATH)

Cette classe est tout à fait standard par ailleurs. Les méthodes qu’on veut rendre accessibles via D-Bus sont implémentées comme des méthodes normales et doivent simplement être répertoriées. Il existe un mécanisme à base de descripteurs XML [11] mais il y a beaucoup plus simple et lisible sous la forme de décorateurs Python. Le plus simple est d’illustrer cela avec la méthode qui donne accès à la fonction d’effacement de l’écran :

    @dbus.service.method('org.pobot.rob.Console.display')
    def clear(self):
        self._lcd.clear()

On voit que le corps de la méthode se contente d’appeler la méthode correspondante de la bibliothèque d’interface du LCD. Le seul ajout est le décorateur, qui stipule que cette méthode est publiée au niveau de D-BUS, et fait partie de l’interface dont le nom est donné en paramètre. Cette notion d’interface est identique à celle de Java et permet de packager les différentes méthodes fournies par un même service en groupes identifiés, dont on peut ensuite gérer individuellement les droits d’accès (d’où leur existence). Même si toutes les méthodes sont dans une unique interface, celle-ci doit quand même être définie.

Bien, et s’il y a des paramètres ? Très simple : on déclare la signature dans le décorateur, comme illustré ci-après avec la méthode qui permet d’écrire un texte à une position donnée du LCD :

    @dbus.service.method('org.pobot.rob.Console.display', in_signature='suu')
    def write_at(self, s, line, col):
        self._lcd.write_at(s, line, col)

La signature est définie par une chaîne dont la syntaxe est documentée ici, au paragraphe "Signature Strings". Dans notre exemple, on fait sans difficulté la relation avec la signature de la méthode Python, à savoir une chaîne, suivi de deux entiers non signés.

Parfait. Et pour les résultats ? Même punition, même motif :

    @dbus.service.method('org.pobot.rob.Console.input', out_signature='as')
    def get_keys(self):
        return self._lcd.get_keys()

Nouveauté : notre méthode retourne un array de strings, ce qui est parfaitement géré par les signatures D-Bus, de même que les structures et les dictionnaires. Le ’a’ qui préfixe le ’s’ signifie qu’il s’agit d’un array. De manière plus générale, ’aX’ signifie "array de X", "X" pouvant être un type simple ou un type complexe (aka container dans les documentations).

A noter au passage que j’ai choisi de packager cette méthode dans une autre interface que précédemment, de manière à différencier les fonctions d’affichages des fonctions d’entrée de donnée. Ce n’était pas indispensable pour notre application, mais ça a permis de tester les mécanismes évoqués précédemment.

Et les signaux au fait ? Ce n’est guère plus compliqué :

    @dbus.service.signal('org.pobot.rob.Console.input', signature='as')
    def key_pressed(self, keys):
        pass

Euh, il ne manque rien là ? La méthode est vide !!!

Non, il ne manque rien. En fait un signal est un point de sortie, sur lequel d’autres vont venir accrocher des écouteurs. Ceux qui ont déjà pratiqué Qt le connaissent déjà avec le concept de SIGNAL et de SLOT de Qt. Le signal est émis sur le bus dès la sortie de la méthode décorée par @dbus.service.signal (vous avez noté que le décorateur n’est pas le même que précédemment). Il est tout à fait possible d’ajouter du code dans la méthode si on a besoin de faire un quelconque traitement.

Pour être plus précis (et exact), ce qui se passe en interne est qu’un message sérialisant le signal va être émis par le binding côté émetteur (ce traitement est ajouté par le décorateur en Python), et pour chaque acteur s’étant enregistré comme abonné à ce signal, le hub va envoyer le signal sérialisé sur le socket qui matérialise la connexion de cet abonné. Le binding coté abonné va alors traduire le message reçu en appel de méthode dans le langage concerné afin d’activer votre handler et lui passer le signal émis.

Autre petite différence par rapport aux méthodes vues précédemment : la signature (si requise) est unique, car une méthode de signal ne retourne rien vis à vis de D-Bus. Le paramètre est donc signature tout court, sans préfixe in_ ou out_. Retenez-le car les erreurs causées par un copier-coller ne se manifestent qu’à l’exécution, et par un message qui n’est pas toujours limpide quand c’est la première fois (expérience vécue bien entendu).

Comment provoquer l’émission du signal ? Très simplement : en appelant la méthode. Ceci est illustré par le code qui se charge de scanner le keypad et d’émettre une notification pour tout changement :

   def _keypad_scanner(self):
        last_keys = None
        while self._scan_keypad:
            keys = self._lcd.get_keys()
            if keys and keys != last_keys:
                self.key_pressed(keys)
                last_keys = keys
            time.sleep(0.1)

Cette méthode est lancée dans un thread par ailleurs, et on peut voir que l’émission du signal est provoquée par l’appel self.key_pressed(keys). Ce n’est pas plus compliqué.

Comment on reçoit un signal ? Eh bien en se connectant sur ce signal depuis le code qui veut l’écouter. C’est aussi simple que ce qui suit :

TODO

TODO : explications

Voilà. Pour compléter tout cela il ne manque plus que le code de lancement. J’ai choisi de le mettre dans le main du module, de manière à ce qu’il soit exécuté par le mécanisme d’activation automatique que nous allons voir bientôt. Cela permet aussi de tester directement le service en le lançant depuis la ligne de commande. Voici ce code :

if __name__ == '__main__':
    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
    gobject.threads_init()
    dbus.mainloop.glib.threads_init()
    _console = LcdConsole()
    loop = gobject.MainLoop()
    loop.run()

Comme déjà évoqué, pour que l’ensemble du mécanisme fonctionne, il est nécessaire d’avoir une boucle d’événements quelque part. C’est l’object de la première instruction. A noter que dans le cas d’applis Qt ou GTK, ça peut être celle de l’IHM, et le binding Python permet de choisir à ce niveau celle qui doit être utilisée. Comme nous ne sommes pas dans le contexte d’une application graphique, nous utilisons la boucle DBusGMainLoop. Vous remarquerez qu’on ne stocke pas de référence à l’instance qui est créée, car nous n’avons pas besoin de la manipuler à priori.

Viennent ensuite deux instructions d’initialisation du threading qu’il est important de ne pas omettre faute de quoi toute utilisation de thread ne marchera pas. Ca ne se plante pas, mais les threads ne sont tout simplement pas démarrés lors de l’appel de leur méthode start(). Là encore, en insistant sur ce point, je vous évite du temps perdu à essayer de comprendre ce qui ne marche pas, car tant qu’on ne fait pas appel aux threads (comme dans la myriade d’exemples à deux balles du Net) il n’y a pas besoin de ces deux appels.

Nous créons ensuite une instance de notre classe qui implémente le service, créons la boucle d’événements et l’activons.

Et au fait : on en sort comment de tout ça ?

Si on ne fait rien et qu’on n’a pas besoin d’en sortir, eh bien on y reste jusqu’à arrêt du système ou au kill du process. Si on veut pouvoir arrêter sur commande le service, ce n’est pas bien compliqué : il suffit d’appeler la méthode quit() de la boucle. Pour aller jusqu’au bout de la démarche, nous avons implémenté la possibilité de provoquer cela via D-Bus également en implémentant cette méthode dans notre classe :

    @dbus.service.method('org.pobot.rob.Console.control')
    def terminate(self):
        self.keypad_autoscan_stop()
        self._lcd.set_backlight(False)
        self._lcd.clear()
        self._lcd.center_text_at('Goodbye', 2)
        loop.quit()

Vous remarquerez qu’elle a été packagée dans une troisième interface, ce qui permet si nécessaire d’en restreindre l’accès par sécurité. L’ensemble du corps de la méthode est sans intérêt ici, le seul point important étant la toute dernière instruction. A noter que loop référence la variable globale au module qui est créée implicitement par l’avant-dernière instruction du main vu juste au-dessus.

 Une petite démo ?

Très simple. On commence par lancer notre service :
$ python lcdsvc.py

Depuis une autre console, nous allons maintenant utiliser l’utilitaire dbus-send pour activer les méthodes mises à disposition. dbus-send fait partie de l’outillage fourni en standard par le paquet dbus. Ces différents outils sont écris en C, ce qui du coup démontre ici parfaitement l’aspect cross-langage de l’approche. Rien n’interdit bien entendu d’utiliser dbus-send dans un script bash par exemple.

Commençons par allumer le rétro-éclairage :

$ dbus-send \
   --system \
   --dest=org.pobot.rob.Console \
   --type=method_call \
   --print-reply \
   /org/pobot/rob/Console/object \
   org.pobot.rob.Console.display.set_backlight \
   boolean:true

Ouch, c’est du lourd là !!! En fait pas tant que cela si on analyse les options une par une, et c’est pour cela que je les ai présentées sur des lignes séparées.

—system
sert à dire qu’on travaille sur le bus système. Si cette option est omise, on est sur le bus session par défaut

—dest=org.pobot.rob.Console
c’est le fameux busname par lequel on peut accéder au process qui implémente le service.

—type=method_call
indique si on fait un appel de méthode ou une émission de signal. Par défaut si omis, il s’agit de l’émission d’un signal

—print-reply
permet d’avoir toutes les informations en retour, y compris les résultats de la méthode si elle en a

/org/pobot/rob/Console/object
le path vers l’objet qui implémente le service, au sein du domaine accessible par le busname

org.pobot.rob.Console.display.set_backlight
le nom qualifié de la méthode (ici : set_backlight) incluant le nom de l’interface dans laquelle elle a été déclarée (ici : org.pobot.rob.Console.display)

boolean:true
les éventuels arguments si nécessaire, chacun suivant la syntaxe type:valeur. Se reporter à la documentation de dbus-send pour toutes les possibilités.

Le résultat de cette commande est :
method return sender=:1.10 -> dest=:1.9 reply_serial=2
Ce sont juste des informations internes, notre méthode ne retournant pas de résultat.

Plus évolué : intéressons-nous maintenant à une méthode qui retourne un résultat, comme l’interrogation de l’état courant du keypad :

$ dbus-send --system --dest=org.pobot.rob.Console --type=method_call --print-reply \
  /org/pobot/rob/Console/object org.pobot.rob.Console.input.get_keys
method return sender=:1.10 -> dest=:1.11 reply_serial=2
  array [
     string "4"
     string "9"
  ]

Je ne détaillerai pas la commande ici, car c’est la même chose que précédemment, mais ce qui est intéressant est ce qui suit la ligne "method..." affichée par dbus-send. Il s’agit d’un pretty-print du résultat de la méthode, en l’occurrence un array de string tel que nous l’avons déclaré dans le code Python, qui contient ici les deux strings ’4’ et ’9’, correspondant au fait qu’au moment du test j’avais appuyé sur ces deux touches du keypad.

Une petite démo des signaux maintenant, qui va nous permettre de présenter un autre outil de la panoplie : dbus-monitor. Comme son nom l’indique, il permet de surveiller ce qui se passe sur les bus. Nous allons l’utiliser ici pour visualiser les signaux émis lorsqu’on appuie sur des touches du keypad :

$ dbus-monitor \
  --system \
  destination=org.pobot.rob.Console \
  type=signal

Détaillons comme précédemment les options utilisées :

—system
nous voulons voir ce qui se passe au niveau du bus système. Si non précisé, dbus-moinitorobservera le bus session

destination=... type=...
c’est un filtre qui permet de ne voir que les messages souhaités, soit ici ceux qui sont sur le busname de notre service, et uniquement de type signal (et pas les appels de méthodes). Se reporter à la documentation pour avoir l’ensemble des mots-clé supportés, car il ne sont pas identiques aux options de dbus-send (par exemple l’équivalent de —dest est destination).

Si on appuie sur les touches du keypad, on peut alors voir s’afficher en temps réel ce qui suit :

signal sender=org.freedesktop.DBus -> dest=:1.21 serial=2 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameAcquired
  string ":1.21"
signal sender=:1.14 -> dest=(null destination) serial=9 path=/org/pobot/rob/Console/object; interface=org.pobot.rob.Console.input; member=key_pressed
  array [
     string "4"
  ]
signal sender=:1.14 -> dest=(null destination) serial=10 path=/org/pobot/rob/Console/object; interface=org.pobot.rob.Console.input; member=key_pressed
  array [
     string "9"
  ]
signal sender=:1.14 -> dest=(null destination) serial=11 path=/org/pobot/rob/Console/object; interface=org.pobot.rob.Console.input; member=key_pressed
  array [
     string "5"
  ]

On peut noter au passage quelques éléments intéressants :
- le tout premier message, émis par org.freedesktop.DBus correspond à la connexion faite par dbus-monitor à notre busname (NameAcquired), qui se passe au moment où il fait l’équivalent de l’instanciation d’un BusName comme nous avons pu le voir au tout début des exemples de code
- la valeur indiquée dans la propriété sender des messages suivants et l’adresse interne générée par D-Bus pour la connexion de notre service, adresse interne qui est ’aliasée’ par le busname déclaré au départ
- la propriété dest des messages est vide (null destination) car un signal est broadcasté par définition et n’a donc pas de destinataire spécifique (à l’inverse d’un message method_call pour des raisons évidentes)
- nous retrouvons l’interface et le nom de la méthode correspondant au signal dans les autres propriétés

Et si pour finir on invoque la méthode org.pobot.rob.Console.control.terminate à l’aide de dbus-send on peut constater dans la console utilisée pour le lancer que le script lancé par la commande python s’est terminé et a rendu la main.

 Activation automatique

Ainsi que cela a été évoqué dans le début de cet article, D-Bus peut gérer automatiquement l’activation des services. Il suffit pour cela d’ajouter un descripteur dans les répertoires :
- /usr/share/dbus-1/services pour les services connectés au bus session
- /usr/share/dbus-1/system-services pour les services connectés au bus système

Illustrons cela par celui de notre service LCD :

$ cat /usr/share/dbus-1/system-services/org.pobot.rob.Console.service
[D-BUS Service]
Name=org.pobot.rob.Console
Exec=/usr/bin/python /home/pi/eurobot_2013/ui/lcdsvc.py
User=pi

Il s’agit d’un fichier au format INI, dont les clés sont assez explicites. "Name" est le nom du service, aka le busname [12]. Nous devons ici inclure la clé "User", du fait qu’il s’agisse d’un service système lancé par conséquent en dehors de toute session utilisateur.

 Il manque quand même quelque chose avant de se quitter

Si vous tentez d’essayer de mettre en pratique tout ceci, eh bien rien ne marchera et vous allez vous faire jeter par des "access denied" et autres injures du genre "t’as des baskets, tu ne rentres pas".

Il manque en effet la définition des autorisations adéquates car n’oublions pas que nous sommes sur le bus système, hyper-sécurisé par défaut.

En fait c’est très simple et cela consiste à ajouter un fragment XML dans le répertoire /etc/dbus-1/system.d/. Vous pourriez être tenté de modifier directement le fichier /etc/dbus-1/system.conf mais n’en faites rien, comme cela est expliqué dans les commentaires. Les concepteurs de D-Bus ont fait les choses très proprement en utilisant le mécanisme assez courant de fichiers placés dans un répertoire xxx.d et qui sont automatiquement pris en compte pour enrichir ou surcharger un fichier de configuration fourni par la distribution. Au moins vous n’avez pas besoin de reporter vos modifications en cas de mise à jour de l’OS.

Voici donc ce fichier :

$ cat /etc/dbus-1/system.d/org.pobot.rob.conf
<?xml version="1.0" encoding="UTF-8"?> <!-- -*- XML -*- -->

<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
 <policy group="robot">
   <allow own_prefix="org.pobot.rob"/>
   <allow send_destination="org.pobot.rob.Brain"/>
   <allow send_destination="org.pobot.rob.Console"/>
 </policy>
</busconfig>

Pas bien compliqué et la documentation disponible sur le sujet est assez explicite (au bout de plusieurs lectures attentives et de quelques expérimentations quand même). Ce qui définit les règles est la balise policy, qui peut être attachée à plusieurs critères : nom d’utilisateur, groupe,... J’ai ici choisi de restreindre l’accès aux services du robot aux membres d’un groupe nommé ’robot’ (original, non ?). J’aurais aussi pu mettre name="*" compte tenu du cadre d’utilisation, ce qui aurait simplement autorisé l’accès à tout le monde, mais nous sommes ici pour apprendre et comprendre comment ça marche. Il faut ajouter mon utilisateur (pi) ainsi que root à ce groupe pour compléter la configuration.

Les tags utilisés correspondent aux déclarations suivantes :
- allow own_prefix=
autorise le "claim" d’un busname commençant par la chaîne indiquée. Il existe aussi la version "own=" mais qui est restreinte au nom indiqué. Ici cela nous permet de ne faire qu’une seule déclaration pour tous les bus names dérivant d’une même racine. D’ailleurs vous aurez constaté que le busname utilisé dans les exemples présentés plus haut est org.pobot.rob.Console, ce qui démontre le fonctionnement de la directive
- allow send_destination=
déclare les interfaces pour lesquelles on autorise l’accès

A noter qu’il existe le pendant de allow qui est deny bien entendu. D’autre part les règles se surchargent dans l’ordre où elles apparaissent et il existe également des règles de précédence qui sont décrites dans la documentation de dbus-daemon.

 Un dernier mot

Il existe une alternative à l’utilisation du bus système en l’absence de bus session, qui consiste à lancer "manuellement" un bus-deamon via la commande dbus-lauch. Cela permet du coup de s’isoler totalement du reste du système, ce qui peut présenter des avantages.

Je n’ai pas totalement terminé d’investiguer cette option, et de toute manière cet article est déjà assez long comme ça. Ce sera donc pour une prochaine fois.

 Conclusion

Voilà. Je vous avais bien dit qu’il s’agissait d’un gros morceau :)

Je ne peux que vous encourager à expérimenter avec D-Bus, que ce soit sur une RasPi ou autre, car il est très puissant et flexible. Il est de plus très robuste car validé par maintenant un bon paquet d’années en exploitation permanente au coeur même de Linux, sans parler des multiples applications qui l’utilisent. Il présente une alternative légère à des solutions plus connues telles que CORBA par exemple.

Sachez également que des portages D-Bus existent également dans les mondes Windows et Mac-OS mais je n’en sais rien de plus, et je doute par ailleurs qu’ils soient autant fiabilisés que la version Linux, ne serait-ce que par le côté anecdotique et marginal que leur utilisation doit avoir. Pour la petite histoire, D-Bus est également présent dans la téléphonie mobile, et des constructeurs comme Nokia ont basé leurs solutions dessus (avant qu’ils ne cède à l’attrait du côté obscur de la Force).

N’hésitez pas à me faire part de toute erreur ou incorrection dans cet article. J’ai essayé d’être le plus rigoureux possible dans la démarche, mais je ne suis qu’un modeste Padawan (ou scarabée selon les références cinématographiques) qui vagabonde sur les chemins de la connaissance ;)


[1ah oui, oui ,oui

[2ce sera peut-être la seule vie qu’il aura en fonction de son niveau d’avancement au moment de la période fatidique ;)

[3bon, pour l’instant tout est en Python

[4il existe également un tas d’outils graphiques équivalents, mais j’ai désactivé X sur ma RasPi vu qu’elle sera utilisée en headless sur le robot, et travaille dessus en ssh

[5Remote Procedure Call

[6ce que je trouve être une grave lacune quand on en mesure la portée

[7comme vous vous en doutez ceci est tiré d’expériences professionnelles vécues

[8je l’utilise dans le cadre d’activités professionnelles de développement de systèmes embarqués

[9Lao Tseu ? Kant ? Churchill ? Non : Spiderman

[10j’ai résisté à la tentation de ne pas le rebaptiser en servicename afin de ne pas rompre avec les documentations publiques, mais le coeur y était quand même.

[11J2EE sors de ce corps !!

[12vous voyez, quand je disais que j’ai dû résister à la tentation de rebaptiser busname en servicename...

Vos commentaires

  • Le 4 janvier 2013 à 16:40, par frederic P En réponse à : D-Bus sur la RasPi

    Super article, merci beaucoup !
    Je ne connaissais pas du tout DBUS, très interressant (et l’article est génialement écrit :-) )

    Pour info, dans le meme genre, il y a la couche de base de ROS.
    Très similaire dans l’approche bien sur, encore que ROS pousse pour l’utilisation de Topics (i.e. des bus multi-producteurs/subscribeurs) plutot que des Services (i.e. RPC) chaque fois que c’est possible, pour limiter encore le couplage : cf les concepts runtime de ROS.

    M’enfin quelque soit le bus utilisé, un choix crucial pour l’interopérabilité et la longévité est celui du contenu des messages.
    Beaucoup de tentatives de standardisations de ce coté, depuis Player jusqu’à ROS, pour un certain nombre de concepts.
    Donc tant qu’à faire, lorsqu’on cherche à definir un nouveau message (surtout avec des concepts ’standards’ tels que des coordonnees, des angles, des consignes d’accélération, etc.), un petit tour de ceux qui existent aide parfois. Par exemple les messages de base de ROS.

    • Le 6 janvier 2013 à 18:10, par Eric P. En réponse à : D-Bus sur la RasPi

      Je suis entièrement d’accord avec toi concernant le problème de la normalisation des messages, et à ça fait partie de mes activités de recherche à titre professionnel concernant les réseaux de capteurs dans le bâtiment.

      Sur ce point, D-Bus présente l’avantage de mettre tout le monde d’accord : on ne définit aucun format de message, pour la simple raison que c’est D-Bus qui le fait au moment du marshalling des appels de méthodes ou d’envoi de signaux. Du coup, la question ne se pose plus, et le bus de message n’est en fait qu’un concept à ce niveau d’utilisation. C’est la même chose que quand on fait du CORBA, Java RMI et consorts : on est totalement isolé du format de communication utilisé par la couche d’implémentation, et tout ce qu’on voit ce sont des appels de méthodes distantes.

    Répondre à ce message

  • Le 5 janvier 2013 à 10:44, par frederic P En réponse à : D-Bus sur la RasPi

    Malgré son indéniable qualité, une erreur s’est glissée dans cet article : "de grands pouvoirs entraînent de grandes responsabilités" : il s’agit d’un citation de Spiderman bien sur, pas Superman :-)

    • Le 6 janvier 2013 à 18:01, par ? En réponse à : D-Bus sur la RasPi

      Bien entendu !!! Comment ai-je été pour écrire une énormité pareille. Bon, faut dire que j’avais peu dormi la nuit d’avant... Je corrige immédiatement.

    Répondre à ce message

Un message, un commentaire ?

modération a priori

Attention, votre message n’apparaîtra qu’après avoir été relu et approuvé.

Qui êtes-vous ?

Pour afficher votre trombine avec votre message, enregistrez-la d’abord sur gravatar.com (gratuit et indolore) et n’oubliez pas d’indiquer votre adresse e-mail ici.

Ajoutez votre commentaire ici
  • Ce formulaire accepte les raccourcis SPIP [->url] {{gras}} {italique} <quote> <code> et le code HTML <q> <del> <ins>. Pour créer des paragraphes, laissez simplement des lignes vides.

Ajouter un document