Club robotique de Sophia-Antipolis

Accueil > Projets, études > Nos réalisations > Balises de localisation > Balises goniométriques 2013 (2ème partie)

Balises goniométriques 2013 (2ème partie)

La réalisation du démonstrateur

lundi 21 octobre 2013, par Eric P.

Le premier article a introduit les constituants de base du démonstrateur en projet.

L’objectif de ce démonstrateur est de fournir un support pédagogique à la présentation d’applications concrètes (et ludiques) de rudiments de trigonométrie. Cette présentation fait partie de conférences et d’ateliers que nous proposons entre autres à l’occasion de la Fête de la Science.

Le démonstrateur

Il est composé de deux parties :

  • un dispositif matériel, comprenant deux balises oscillantes décrites dans l’article précédent, montées avec leur électronique de contrôle sur une platine à accrocher sur un tableau mobile (type paperboard), ayant pour fonction de localiser la position d’une cible mobile pouvant être placée et déplacée sur le tableau
  • une application de visualisation graphique du fonctionnement des balises et de la position de la cible, calculée à partir des directions des échos fournies par les balises

Les deux parties sont connectées en WiFi, l’électronique de contrôle assurant la fonction d’access point, afin de garantir l’indépendance en termes de réseau grâce à la possibilité d’y connecter directement l’ordinateur hébergeant l’application graphique sans contrainte particulière.

On peut voir l’ensemble en fonction lors de la dernière Fête de la Science :

Les balises et leur contrôleur

Les balises sont semblables au modèle à deux capteurs déjà présenté en première partie. Un laser ligne y a été ajouté, afin de tracer sur le tableau une ligne visualisant la direction de pointage de la balise.

Elles sont contrôlées par une Raspberry Pi montée elle aussi sur la platine. Celle-ci assure les fonctions suivantes :

  • piloter les AX12 qui actionnent les balises
  • contrôler l’activation des capteurs de proximité (afin de leur éviter une usure prématurée par un fonctionnement trop prolongé)
  • idem pour les lasers lignes
  • récupérer les signaux de détection fournis par les capteurs
  • communiquer via le Wifi à l’application de visualisation les directions (angles) des échos détectés, ainsi que la position en temps réel des têtes oscillantes
  • recevoir et traiter les commandes de contrôle (activation du balayage, activation des lasers et des capteurs,...) envoyées par l’application de visualisation

Comme décrit dans l’article précédent, l’interface entre les AX12 et la RasPi est assurée par l’USB2AX déjà décrite dans nos colonnes et les I/O sont gérées par la carte 32 I/O de AB Electronics. Ces I/O sont utilisées pour les fonctions suivantes :

  • lecture des signaux des 4 capteurs
  • commande d’activation des 4 capteurs (via leur alimentation)
  • commande d’activation des 2 lasers (via leur alimentation)

La carte I/O ne pouvant fournir suffisamment de courant pour les périphériques, ses sorties attaquent un driver ULN2803. Enfin, de manière à permettre d’arrêter proprement la RasPi, le système de bouton d’arrêt décrit dans cet article a été adjoint au dispositif (en fait, il a été développé pour lui). Le tout, ainsi que la (copieuse) connectique est regroupé sur un petit PCB annexe.

L’ensemble des cartes est fixé sur la platine globale :

Le tout est alimenté par une petite alimentation de type PC (en réalité, récupérée sur un photocopieur endommagé par la foudre) qui fournit le 5V pour la RasPi et le 12V pour les AX12. Elle est montée sur une platine (réalisée dans un morceau de plateau d’alimentation en papier du même photocopieur) suspendue au dos du tableau de présentation.

Le logiciel contrôleur côté RasPi

Il implémente un serveur de commandes, qui gère un protocole ASCII simple pour contrôler les mécanismes depuis l’application graphique distante. Un petit extrait ci-dessous du log de l’application illustre cela :

$ ./beacons-gui.py 
08:33:27.999 [I] main            > using beacons controller pibot-beacons.local (192.168.42.1)
08:33:28.297 [I] main            > application started
08:33:28.297 [I] app             > starting beacons event listener
08:33:28.297 [I] listener        > using 2 sensor(s)
08:33:28.298 [I] listener        > start listening for controller events on port 1235...
08:33:28.298 [I] app             > connecting to beacons controller
08:33:28.298 [I] listener        > listening for controller events...
08:33:28.303 [I] app             > retrieving controller identity
08:33:28.437 [I] app             > --> ('pibot-beacons.local', [], ['192.168.42.1'])
08:33:28.437 [I] app             > registering as beacons event listener
08:33:28.437 [I] app             > cmd: register
08:33:28.450 [I] listener_hdl    > client connected : ('192.168.42.1', 51110)
08:33:28.456 [I] app             > .... OK
08:33:28.456 [I] app             > starting application main loop
08:33:35.115 [I] app             > cmd: scan L R
08:33:35.158 [I] app             > .... OK
08:33:40.847 [I] app             > cmd: laser L:1 R:1
08:33:40.914 [I] app             > .... OK
08:33:43.600 [I] app             > cmd: laser L:0 R:0
08:33:43.626 [I] app             > .... OK
08:33:44.517 [I] app             > cmd: stop L R
08:33:45.055 [I] app             > .... OK
08:33:48.392 [I] app             > cmd: stop
08:33:48.567 [I] app             > .... OK
08:33:48.567 [I] app             > cmd: unregister
08:33:48.573 [I] listener_hdl    > connection closed by peer
08:33:48.573 [I] listener_hdl    > loop exited
08:33:48.578 [I] app             > .... OK
08:33:48.578 [I] app             > cmd: disc
08:33:48.587 [I] app             > .... OK
08:33:48.587 [I] listener        > terminate requested
08:33:48.976 [I] listener        > no more listening
08:33:48.976 [I] main            > the end

Aucune bibliothèque particulière (comme Twisted par exemple) n’a été utilisée ici, mais une programmation directe avec les sockets. Il n’y a pas de difficulté particulière, si ce n’est que j’ai découvert pendant mon apprentissage de la chose que l’implémentation des serveurs proposée par la bibliothèque standard ferme la connexion à la fin de l’exécution du handler de traitement des données reçues. Ceci ne correspond pas au mode recherché, dans la mesure où nous voulons que la connexion persiste pendant toute la durée de la session, c’est à dire de l’exécution de l’application cliente.

La méthode que j’ai utilisée pour résoudre ce point est d’écrire le handler comme une boucle, qui ne sort qu’à réception de la commande disc correspondant à la fin de la session client.

En plus d’exécuter les commandes en provenance de l’application graphique et de piloter le matériel en conséquence, le contrôleur émet en permanence des événements pour notifier la position angulaire des balises, le début et la fin de détection des échos,... Ces événements ont un format analogue aux commandes, et sont envoyés sur un socket retour qui est créé en traitement de la commande register émise en début de session par l’application graphique. La commande register porte ce nom, car elle correspond à l’enregistrement d’un listener d’événements.

Le logiciel graphique

L’application est développée sur la base de pyGame pour la gestion des aspects graphiques, et utilise les sockets en direct pour la communication avec le contrôleur. Elle aussi implémente donc un serveur, qui est en attente des événements envoyés par le contrôleur et les utilise pour mettre à jour l’affichage.

La position du mobile est calculée à réception des événements d’échos. Le résultat, ainsi que les positions angulaires des balises fournies régulièrement par le contrôleur servent à mettre à jour le graphisme. Au chapitre des interactions avec l’utilisateur :

  • un clic sur une balise l’active ou l’arrête selon son état courant
  • shift-clic agit sur les deux balises en ensemble
  • ’S’ active les deux balises, et ’s’ les arrête
  • ’L’ allume les deux lasers de visualisation, et ’l’ les éteint
  • ’h’ affiche l’anti-sèche des commandes

Par sécurité, les lasers s’éteignent automatiquement au bout de 30 secondes (vu leur prix il ne faut pas trop leur en demander). Les balises s’arrêtent également d’elles-mêmes au bout de 5 minutes (par égard pour les oreilles de voisins).

Un peu de théorie

Comme je sais que vous êtes insatiables, voici la théorie mathématique (oh, que c’est pompeux !!) qui est derrière les calculs de position.

La démarche mathématique utilisée
Extrait de la conférence ’Robotique et mathématiques" présentée aux classes de lycée
(c) E. PASCUAL pour POBOT

Il s’agit de quelques uns des slides utilisés pour une des conférences que nous donnons dans les établissements scolaires, notamment pendant la Fête de la Science, et pour laquelle le dispositif décrit dans cet article a été développé.

En prime

Petite particularité du système : afin de ne pas devoir mémoriser des adresses réseau dans la cas où le contrôleur et le poste interface utilisateur font partie d’un réseau plus étendu (par exemple plusieurs démonstrateurs différents pouvant être contrôlés depuis un unique PC), j’ai testé l’utilisation d’Avahi pour publier des services côté contrôleur, et en faire la recherche côté client. Avahi est une des implémentation du protocole mDNS/DNS-SD. Il se trouve que la distribution Occidentalis inclut Avahi, et que par ailleurs il est présent par défaut dans pas mal de distributions Linux. Des bibliothèques sont également disponibles sous les autres OS, mais je ne peux pas en dire plus sur le sujet.

Histoire de simplifier les choses au maximum, et sachant que nous sommes en Linux (d’office sur la RasPi et par choix personnel sur mes machines), je me suis contenté d’appeler les commandes en ligne correspondantes via les services du packages subprocess de Python. En enrobant le tout dans un petit module, l’utilisation en est encore simplifiée :

#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
""" A couple of helper functions and classes for using Avahi tools
(instead of going trhough D-Bus path, which is somewhat less simple).
 
Note: this will run on Linux only, Sorry guys :/
"""
 
import sys
if not sys.platform.startswith('linux'):
    raise Exception('this code runs on Linux only')
 
import subprocess
 
def whereis(bin_name):
    """ Internal helper for locating the path of a binary.
 
    Just a wrapper of the system command "whereis".
 
    Parameters:
        bin_name:
            the name of the binary
 
    Returns:
        the first reply returned by whereis if any, None otherwise.
    """
    output = subprocess.check_output(['whereis', '-b', bin_name])\
                .splitlines()[0]\
                .split(':')[1]\
                .strip()
    if output:
        # returns the first place we found
        return output.split()[0]
    else:
        return None
 
def find_service(svc_name, svc_type):
    """ Finds a given Avahi service, using the avahi-browse command.
 
    Parameters:
        svc_name:
            the name of the service, without the leading '_'
 
        scv_type:
            the type of the service, without the leading '_' and the
            trailing "._tcp' suffix
 
    Returns:
        a list of matching locations, each entry being a tuple
        composed of:
            - the name of the host on which the service is published
            - its IP  address
            - the port on which the service is accessible
        In case no match is found, an empty list is returned
 
    Raises:
        AvahiNotFound if the avahi-browse command is not available
    """
    _me = find_service
    try:
        cmdpath = _me.avahi_browse
    except AttributeError:
        cmdpath = _me.avahi_browse = whereis('avahi-browse')
        if not cmdpath:
            raise AvahiNotFound('cannot find avahi-browse')
 
    locations = []
    output = subprocess.check_output([cmdpath, '_%s._tcp' % svc_type, '-trp']).splitlines()
    for line in [l for l in output if l.startswith('=;')]:
        _netif, _ipver, _name, _type, _domain, hostname, hostaddr, port, _desc = \
                line[2:].split(';')
        if not svc_name or _name == svc_name:
            locations.append((hostname, hostaddr, int(port)))
 
    return locations
 
class AvahiService(object):
    """ A simple class wrapping service publishing and unpublishing. """
    _cmdpath = whereis('avahi-publish-service')
 
    def __init__(self, svc_name, svc_type, svc_port):
        """ Constructor.
 
        Parameters:
            svc_name:
                the name of the service
 
            svc_type:
                the type of the service. The leading '_' and trailing
                '._tcp' suffix can be omitted for readability's sake.
                They will be added automatically if needed.
 
        """
        if not self._cmdpath:
            raise AvahiNotFound('avahi-publish-service not available')
        self._process = None
 
        self._svc_name = svc_name
        if not (svc_type.startswith('_') and svc_type.endswith('_tcp')):
            self._svc_type = '_%s._tcp' % svc_type
        else:
            self._svc_type = svc_type
        self._svc_port = svc_port
 
    def publish(self):
        """ Publishes the service.
 
        Starts the avahi-publish-service in a separate process. Do nothing
        if the service is already published.
        """
        if self._process:
            return
 
        self._process = subprocess.Popen([
            self._cmdpath,
            self._svc_name, self._svc_type, str(self._svc_port)
        ], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        if not self._process:
            raise AvahiError('unable to publish service')
 
    def unpublish(self):
        """ Unpublishes the service, if previously published.
 
        Do nothing if the service is not yet publisehd.
        """
        if self._process:
            self._process.terminate()
            self._process = None
 
class AvahiError(Exception):
    """ Root of module specific errors. """
    pass
 
class AvahiNotFound(AvahiError):
    """ Dedicated error for command not found situations. """
    pass


L’étude de ce source permet en outre de découvrir plusieurs des outils avahi en ligne de commande, dont avahi-browse, qui grâce à ses diverses options se prête très bien au parsing automatique de sa sortie et est une alternative ligne de commande à avahi-discover pour les réfractaires à la souris, ou ceux qui ne veulent pas installer encore un autre package, avahi_discover étant distribué dans un paquet distinct de avahi-utils de manière permettre à ce dernier d’être installé dans une configuration headless.

Pour compléter la documentation, les options de avahi-browse utilisées ici sont :

  • -t : termine la commande dès réception du premier lot de réponses (si omise, avahi-browse reste en attente d’autre réponses, de manière à notifier d’éventuelles nouvelles apparitions de services
  • -r : résout les adresses IPv4 et les affiche
  • -p : formatte la sortie en mode parseable, avec une ligne par détection, dont les champs sont séparés par des points-virgule et les caractères spéciaux remplacés par leur représentation en octal

Conclusion

Voilà, pas grand-chose à en dire de plus.

Les plus curieux pourront aller voir le code sur notre repository Github, l’application étant dans le dossier apps/beacons.

Et pour finir une illustration en vidéo (version HD disponible en cliquant sur "HD" dans le player) :

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 champ accepte les raccourcis SPIP {{gras}} {italique} -*liste [texte->url] <quote> <code> et le code HTML <q> <del> <ins>. Pour créer des paragraphes, laissez simplement des lignes vides.