URBI Tutorial pour Urbi 1.5

(book compiled from )

Jean-Christophe Baillie

Mathieu Nottale

Benoit Pothier

Nicolas Despres

Traduction de l'anglais Antoine (zelig) Hue

This document is released under the Attribution-NonCommercial-NoDerivs 2.0 Creative Commons licence (http://creativecommons.org/licenses/by-nc-nd/2.0/deed.en).


Table of Contents

1. Introduction
2. Installer URBI
Préparer le memorystick pour Aibo
3. Premiers pas
Affecter et lire une valeur associée à un moteur
Régler la vitesse, la durée ou la gestion sinusoïdale des mouvements
A la découverte des variables
Structure générale des variables
Les valeurs d'un device et l'alias .val
Créer une variable globale
Les expressions
Les listes
Exécuter des commandes en parallèle
Affectations conflictuelles
Variables et propriétés utiles des devices
Quelques commandes utiles
4. Fonctions plus avancées
Le branchement conditionnel et les boucles
if
while
for
loop
Les mécanismes de capture d'évènements
at
whenever
wait, waituntil
timeout, stopif, freezeif
Les tests coulés
Emettre un événement
Evénements simples
Les événements avec paramètres
Durée d'un événement
Evénements pulsés: la commande every
Etiquettes, drapeaux et contrôle des commandes
Le regroupement d'objets
Définir une fonction
Messages d'erreur et messages-système
5. Les objets avec URBI
Définir une classe
Méthodes virtuelles et attributs
Les groupes
La diffusion
6. L'exemple de la détection de balle
Détection de la balle
Le programme principal
Programmer par un graphe de comportement
Contrôler l'exécution du comportement
7. Images et sons
Lire une valeur binaire
Affecter une valeur binaire
Attributs associés
Exemples d'opérations binaires
8. La liburbi en C++
Qu'est-ce que la liburbi?
Les composants et liburbi
Premiers pas
Envoyer une commande
Envoyer une donnée binaire
Recevoir des messages
Les types de données
Opérations synchrones
Lecture synchrone de la valeur d'un device
Obtenir une image de façon synchrone
Obtenir du son de façon synchrone
Fonctions de conversion
L'exemple d'urbiimage
9. Créer des composants: l'architecture UObject
UObject
Les bases
Ajouter des attributs
Lier une fonction ou un évenement
Minuteurs
Types binaires avancés
L'attribut load
L'attribut remote
L'exemple de colormap
En pratique, comment profiter d'un UObject?
Comment installer le kit de développement SDK pour construire/lier des composants pour votre robot ?
Comment diffuser un de vos composants et les rendre utilisable par les autres?
10. Mettre tout cela ensemble
Exemples d'utilisations classiques
A. Copyright

List of Figures

4.1. Une hiérarchie typique de device moteurs
6.1. Le graphe de comportement de la détection de balle
10.1. L'architecture générale d'URBI, en mettant tout ensemble

Chapter 1. Introduction

URBI (Universal Robotic Body Interface) est un langage de script conçu pour fonctionner selon un mode client/serveur dans le but de contrôler un robot, ou plus largement, tous les types d'appareils disposant de moteurs et de capteurs. Comme nous allons le voir dans ce tutoriel, URBI est plus qu'un simple outil de pilotage, il est un système universel de contrôle du robot apportant des fonctionnalités supplémentaires via des plugins et autorisant la création d'applications interactives complexes ouvertes.

Les principales qualités d'URBI sont les suivantes :

  • Simplicité: Facile à comprendre et puissant à la fois, URBI convient aussi bien à l'enseignement qu'au développement d'applications professionnelles.

  • Flexibilité: Indépendant du robot, de l'OS, de la plateforme, URBI est également interfaçable avec de nombreux langages (C++, Java, Matlab...).

  • Modularité: L'architecture de composants orientés objet permet d'étendre les possibilités d'URBI grâce à une multitude de langages. Les composants peuvent être externes au serveur URBI (remote) ou intégrés (plugin).

  • Traitements parallèles: Exécution parallèle de commandes, gestion des accès concurrentiels aux variables, programmation évenementielle, ...

Le point probablement le plus important de ce tutoriel est le suivant : URBI a été conçu dès le début avec un souci constant de simplicité. Il n'y a pas de philosophie ou d'architecture complexe à assimiler. Il est compréhensible en quelques minutes et peut être utilisé immédiatement. Que vous développiez une application se contentant de bouger quelques articulations du robot ou une application implémentant une intelligence artificielle complexe, URBI vous fournira les outils pour vous simplifier la vie.

URBI est disponible pour beaucoup de robots et leur nombre augmente. Actuellement, il existe une version d'URBI pour Aibo, pour l'humanoïde HRP-2, pour le simulateur universel Webots, les robots Pioneer, l'iCat de Philips et d'autres humanoïdes vont suivre.

La compatibilité avec le simulateur Webots signifie qu'il est possible de passer du véritable robot à sa simulation via un simple changement d'adresse IP, et cela rend URBI particulièrement adapté à cet usage.

Dans ce tutoriel, nous avons essayé de faire une description progressive d'URBI qui s'étend des commandes simples pour les moteurs à de la programmation plus complexe ainsi qu'aux composants logiciels intégrés. Tout ceci a été rédigé dans le but d'être compréhensible par des personnes ayant peu ou pas d'expérience en matière de robotique et de programmation (exception faite des sections traitant du C++ qui exigent un minimum d'aisance avec ce langage). Cependant, de temps en temps, nous avons fourni des explications ou des compléments qui resteront probablement opaques au lecteur lambda. Ces commentaires sont représentés par un petit signe académique comme illustré à gauche de ce texte.

Chapter 2. Installer URBI

Nous ne pouvons montrer en détail dans ce tutoriel comment installer URBI pour chaque robot actuellement supporté, mais l'idée générale est d'avoir un serveur URBI chargé et démarré sur votre robot. La marche à suivre est normalement décrite dans le fichier INSTALL de l'archive que vous avez téléchargée. Idéalement, URBI est préinstallé sur votre robot.

Etant donné que nous allons donner beaucoup d'exemples appliqués à l'Aibo dans ce tutoriel, nous fournissons ici les instructions d'installation d'URBI sur Aibo. Nous expliquerons également comment installer URBILab qui est un client graphique multi-environnements à la fois simple et pratique pour remplacer telnet.

Préparer le memorystick pour Aibo

Tout d'abord, téléchargez le memorystick correspondant à votre robot. Deux possibilités s'offrent à vous pour l'instant :

Instructions rapides :

Décompressez l'archive et copiez le contenu du dossier MS-xxx sur un memorystick vierge, puis mettez le fichier WLANCONF.TXT à jour en l'adaptant à la configuration de votre réseau.

Instructions détaillées :

  1. Décompressez l'archive correspondant à votre Aibo. Vous devriez obtenir un dossier nommé MS-ERS7 ou MS-ERS200. Entrez dans ce répertoire.

  2. A partir du dossier MS-ERS7 (ou MS-ERS200), allez dans OPEN-R/SYSTEM/CONF. Il devrait y avoir un fichier WLANCONF.TXT (sinon vous devez le créer), pour configurer le réseau convenablement. Il n'existe aucune documentation officielle sur la façon de modifier WLANCONF.TXT, mais voici un exemple dont vous pouvez vous inspirer :

    HOSTNAME=aibo.mondomaine.com
    ETHER_IP=192.168.1.111 # <— votre IP ici
    #
    # Réseau sans fil
    #
    ESSID=0a3902 # <— votre SSID ici
    WEPENABLE=1 # <— WEP ou non
    WEPKEY=0x4B2241785B # <— la clé: hexadécimale
    #WEPKEY=ABCDE # <— en ASCII pour ERS2xx
    APMODE=1
    
    #
    # Réseau IP
    #
    USE_DHCP=0
    SSDP_ENABLE=1
    
    # Cette partie est facultative
    # Votre configuration réseau ici —>
    ETHER_NETMASK=255.255.255.0
    IP_GATEWAY=192.168.0.3
    DNS_SERVER_1=192.168.1.1
    

    Vous pouvez utiliser URBI sur un Aibo sans le réseau si vous ne disposez pas d'équipement Wi-Fi en appelant vos programmes depuis le fichier URBI.INI.

  3. Copiez le contenu du dossier MS-ERS7 ou MS-ERS200 à la racine d'un memorystick rose spécialement conçu pour la programmation (un "PMS"). [1] Prenez garde à ne pas utiliser le memorystick contenant Aibo Mind ou un memorystick bleu : à l'heure actuelle, il est indispensable d'acheter un memorystick spécial programmation à Sony, ce n'est malheureusement pas inclus dans les accessoires de base. Une fois fait, insérez le stick et démarrez le robot. Votre robot URBI est prêt.

    Vous pouvez ouvrir une connexion telnet [2] sur le port 54000 du robot pour vous assurer que tout est OK :

    telnet aibo.gostai.com 54000
    

    Vous devriez voir s'afficher le message d'accueil d'URBI qui se présente comme suit:


 [00014373:start] *** **********************************************************
[00014373:start] *** URBI Language specif 1.0 - Copyright 2005-2007 Gostai SAS
[00014373:start] *** URBI Kernel version 1.5 rev.1896
[00014373:start] ***
[00014373:start] ***   URBI Engine version 1.5 rev. 477
[00014373:start] ***      (C) 2006-2007 Gostai SAS
[00014373:start] ***
[00014373:start] *** URBI comes with ABSOLUTELY NO WARRANTY;
[00014373:start] *** This software can be used under certain conditions;
[00014373:start] *** see LICENSE file for details.
[00014373:start] ***
[00014373:start] *** See http://www.urbiforge.com for news and updates.
[00014373:start] *** **********************************************************
[00014373:ident] *** ID: U135515824

Un des avantages de l'architecture client/serveur d'URBI est que vous pouvez immédiatement envoyer des commandes à votre robot via un simple client telnet. Bien entendu, il est envisageable d'interfacer URBI avec un programme écrit en C++ ou en Java, comme nous le verrons prochainement en parlant de liburbi (chapitre La liburbi en C++). Nous allons pour l'instant nous contenter d'une interface telnet.

Toutefois, telnet est quelque peu spartiate (et ne marche pas toujours bien sous Windows [3] ) et nous avons développé un logiciel multi-plateforme disposant d'une interface graphique que nous avons appelé URBI Remote et que nous vous encourageons à utiliser. URBI Remote est gratuit et mis à disposition sous license GNU-GPL. Vous pouvez le télécharger ici (disponible fin 2007) :

http://www.urbiforge.com/index.php?option=com_content&task=view&id=75&Itemid=136

D'autres applications graphiques tierces-parties comme Aibo-Telecommande peuvent être téléchargées dès maintenant sur urbiforge.com.



[1] MS-ERS7 et MS-ERS200 ne devront pas se trouver sur le memorystick, seuls le contenu de leurs répertoires devront être dupliqués à la racine du memorystick.

[2] Remarque à destination des utilisateurs de Windows : La commande telnet de Windows ne fonctionne pas très bien et vous devriez plutôt utiliser Aibo-Telecommande ou URBIRemote plutôt que telnet. Toutefois telnet marche à la perfection avec MacOSX ou linux.

[3] La situation s'améliore si vous utilisez Cygwin, sans quoi les retours à la ligne sont mal interprétés par la commande telnet fournie dans Windows.

Chapter 3. Premiers pas

Dans ce qui suit, nous allons étudier des exemples prévus pour l'Aibo, mais vous pouvez les transposer pour votre propre robot. Chaque partie du robot (capteurs, moteurs, caméra, ...) est un objet et possède un nom. Par exemple, en ce qui concerne l'Aibo, il existe deux moteurs dans sa tête qu'URBI nomme headPan et headTilt. L'objet associé à la caméra est appelé camera. Par la suite, nous emploierons le terme device pour désigner un objet qui gère une partie matérielle du robot. Ainsi nous parlerons du camera device, des motor devices, etc.

Vous trouverez la liste des devices pour votre robot en consultant sa documentation associée sur (http://www.gostai.com/doc.php), ou simplement en tapant la commande group objects;.

Affecter et lire une valeur associée à un moteur

Nous allons utiliser les moteurs dans ce qui suit, donc, avant toute chose, nous les mettons en route :

motors on;

motors off est bien sûr également disponible, comme normalement n'importe quel device ou objet avec nom_du_device on/off. Commençons par déplacer le moteur HeadPan à 30 degrés:

headPan = 30;

Maintenant, demandons quelle est la valeur du device HeadPan:

headPan;
[00177648] 30.000000

Le serveur répond avec un message (écrit ici en italique pour le distinguer des commandes) précédé d'une marque temporelle (timestamp) et d'une étiquette (tag) entre crochets. Ici la commande n'a été associée à aucune étiquette, par conséquent le terme notag est utilisé par défaut. Il est très simple d'associer une étiquette à une commande en URBI. Il suffit de précéder la commande d'un mot suivi du symbole deux-points (:):

monetiquette << headPan;
[00300553:monetiquette] 30.000000

Le message possède maintenant l'étiquette monetiquette. Cela va s'avérer indispensable lorsqu'il s'agira de déterminer qui envoie quoi dans un contexte de commandes s'exécutant en parallèle, ou pour arrêter une commande en cours d'exécution en tâche de fond.

Vous pouvez essayer d'agir ainsi sur différents moteurs de l'Aibo tels legRF1 ou tailTilt, ou bien vous amuser avec les lumières (leds) telles ledF1 ou ledBMC, ou bien encore lire les valeurs de capteurs comme le détecteur de distance distanceNear ou l'accéléromètre avec accelX, accelY et accelZ. La syntaxe est toujours la même: device = value;.

En réalité device = value est compris par URBI comme device.val = value; qui affecte le paramètre val du device à value. Mais, afin de simplifier encore les choses pour les débutants, tous les devices d'Aibo ont un alias construit de la façon suivante:

alias headPan headPan.val

Du coup, vous n'avez pas à vous préoccupper d'ajouter le paramètre val pour les travaux basiques sur les moteurs. Il est toutefois possible de retirer ces alias (définis dans le fichier URBI.INI) avec la commande unalias.

Régler la vitesse, la durée ou la gestion sinusoïdale des mouvements

Les exemples précédents affectent la valeur d'un device aussi rapidement que le matériel le peut. Bien entendu, vous souhaitez certainement faire quelquechose de plus élaboré comme atteindre une certaine valeur dans un temps donné (en millisecondes):

headPan = 30 time:3000;

qui va atteindre la valeur 30 (degrés) en 3000ms. Quand vous avez à exprimer une valeur temporelle avec URBI, vous pouvez toujours préciser explicitement l'unité que vous souhaitez employer:

headPan = 30 time:3s;
headPan = 30 time:3000ms;
headPan = 30 time:3m;
headPan = 30 time:3h26m15s;

Vous pouvez utiliser des jours (d), des heures (h), des minutes (m), des secondes (s) et des millisecondes (ms), avec des valeurs décimales. Par défaut, l'unité est la milliseconde si aucune unité n'est précisée ou lorsqu'une variable est utilisée.

D'autrepart, vous pouvez également régler la vitesse à la laquelle la valeur doit être atteinte, exprimée en unité/s:

headPan = 30 speed:1.4;

Ou l'accélération (exprimée en unité/s²):

headPan = 30 accel:0.4;

Une manière très utile d'affecter une variable avec un profil dynamique est d'employer l'oscillation sinusoïdale:

headPan = 30 sin:2s ampli:20,

Cela va faire osciller le device HeadPan autour de 30 degrés avec une amplitude de 20 degrés et une période de 2 secondes. Remarquez que la commande se conclue par une virgule et non un point-virgule. Nous expliquerons ceci en détail plus tard, mais, en deux mots, la raison est que l'oscillation ne termine jamais et la virgule signifie en quelque sorte "mais fais tourner ceci en tâche de fond" pour permettre aux commandes suivantes d'être exécutées. Autrement, avec un point-virgule, aucune autre commande ne pourrait ensuite être exécutée puisque l'oscillation ne termine jamais. Il s'agit d'une erreur courante de la part des débutants en URBI.

time, speed et sin sont appelés des modificateurs. Il en existe beaucoup d'autres comme phase, getphase ou smooth. Rendez-vous dans le document "URBI Language Specification" pour une liste exhaustive des modificateurs.

Un modificateur particulièrement puissant est une fonction qui affecte une fonction temporelle complexe en tant que variable de trajectoire. Tout ceci est décrit dans "URBI Language Specification" et ne sera disponible qu'à partir de la version 2.0 des serveurs.

A la découverte des variables

Avec URBI, vous pouvez utiliser des variables. Affecter une valeur à x suffit à créer une variable nommée x qui sera locale à votre connexion:

x = 4;
x;
[00520072] 4.000000

Structure générale des variables

Avec la version 1.0 du noyau URBI, les noms de variable sont toujours de la forme préfixe.suffixe et lorsqu'aucun préfixe n'est donné, un préfixe local à votre connexion est silencieusement ajouté afin que ce x n'interfère pas avec le x d'une autre connexion.

Par exemple, quand vous tapez x, URBI va en réalité utiliser U596851624.x dans sa mémoire, U596851624 étant l'identificateur de votre connexion courante (celle dans laquelle vous avez tapé x). De la même manière, les appels de fonction possèdent un espace local de travail (namespace) afin que d'éventuels appels récursifs puissent cohabiter sans risque d'interférence. Tout ceci va être entièrement revu dans URBI 2.0, avec une gestion avancée des noms de variables et d'espaces de travail.

Les valeurs d'un device et l'alias .val

Comme nous l'avons vu précédemment, il existe une exception importante à la règle qui dit que les variables sans préfixe sont locales: quand vous tapez headPan, URBI ne le traite pas comme une variable locale mais applique un alias qui traduit l'expression en headPan.val, qui est une variable URBI standard contenant la valeur du device. Donc en réalité, headPan ne réfère pas à une variable locale mais à la variable globale headPan.val. Les alias sont habituellement définis dans le fichier URBI.INI.

Créer une variable globale

Il n'y a pas vraiment de concept de variables locales ou globales dans cette version d'URBI. Tout est de la forme préfixe.suffixe. Sans préfixe, la variable est locale à la connexion mais vous pouvez inventer votre propre préfixe pour rendre votre variable "globale":

monprefixe.x = "salut";

En fait, monprefixe peut être vu et défini comme un objet URBI. Nous le verrons dans le chapitre Les objets avec URBI qui détaille l'aspect orienté objet d'URBI.

Les expressions

Le type d'une variable (numérique, chaîne de caractères, liste ou même binaire, comme nous le verrons plus tard) est automatiquement déduit par URBI.

Vous pouvez évaluer des expressions complexes, composées de variables ou de fonctions standard comme sin, cos ou random (voir le document "URBI Specification" pour consulter la liste complète):

x = pi / 2;
calc << sqrt(1 + sin(x));
[00593076:calc] 1.414214

Une propriété intéressante est que les modificateurs présents au sein d'affectations complexes sont constamment ré-évaluées pour que, dans le cas où ils contiennent des variables, la valeur du modificateur puisse évoluer dans le temps en même temps que la variable évolue de son côté. Voici un exemple qui affecte à x une oscillation sinusoïdale à l'intérieur d'un champ sinusoïdal compris entre 15 et 25:

the_amplitude = 20 sin:10s ampli:5,
x = 0 sin:2s ampli:the_amplitude,

D'importantes interactions entre variables et devices peuvent ainsi être établies.

Les listes

Avec URBI, vous pouvez stocker des éléments dans une liste, simplement en les encadrant par des crochets:

malist = [1, 2, 35.12, "hello"];
malist;
[00636417] [1.000000,2.000000,35.119999,"hello"]

Ajouter de nouveaux éléments ou des listes entre elles se réalise simplement:

malist = [1, 2] + "hello";
malist;
[00034647] [1.000000,2.000000,"hello"]
x = 1;
malist + [45, x];
[00101011] [1.000000,2.000000,"hello",45.000000,1.000000]

Pour accéder successivement à chaque élément d'une liste, utilisez la commande foreach:

list = [1, 2];
for n in list { echo (n) };
[00012407] *** 1.000000
[00012407] *** 2.000000

Pour des raisons purement techniques, le code exécuté dans une commande foreach doit être encadré par des accolades, même s'il n'est composé que d'une commande.

Vous pouvez accéder à n'importe quel élément en fournissant sa position dans la liste, comme avec les tableaux de la majorité des langages de programmation:

malist = [1, 2, "hello"];
malist[2];
[00013799] "hello"

Pour accéder aux éléments d'une liste contenue elle-même dans une autre liste, utilisez des index multiples comme maliste[3][4].

Enfin, vous pouvez demander le premier élément de la liste (ce que l'on nomme la tête ou head) et le reste (ce que l'on nomme la queue ou tail):

malist = [1, 2, "hello"];
head(malist);
[00014214] 1.000000
tail(malist);
[00019686] [2.000000,"hello"]

Exécuter des commandes en parallèle

Une commande URBI peut durer un certain temps. C'est une nouveauté parmi la plupart des autres langages. Nous avons vu précédemment que l'on peut affecter des valeurs avec une certaine durée, une certaine vitesse, voire même avec une évolution sinusoïdale sans fin. Il existe de nombreuses façons de faire fonctionner ces commandes en parallèle. Nous avons déjà vu comment le faire en utilisant une virgule pour séparer les commandes au lieu d'un point-virgule.

Il y a une autre manière pour indiquer que les commandes doivent s'exécuter en parallèle: utiliser & :

x=4 time:1s & y=2 speed:0.1;

La différence avec le séparateur virgule est qu'ici on force les deux commandes à démarrer exactement au même moment. En particulier, cela signifie que la première commande ne peut démarrer tant que la seconde n'est pas complètement disponible. Ainsi, taper x=4 time:1s & dans la console ne produira rien car URBI attendra alors de savoir ce qui suit, après le & (c'est la raison pour laquelle le séparateur virgule existe, car ce dernier est moins contraignant et permet de lancer les commandes intéractivement).

Dans la même idée, les commandes peuvent être lancées en série, exactement l'une après l'autre, en utilisant un séparateur | (appelé pipe, tuyau en anglais):

x=4 time:1s | y=2 speed:0.1;

Il n'y aura aucun temps mort entre les deux commandes donc, là encore, URBI attend que la seconde commande soit disponible: la seconde commande doit démarrer exactement après la première, par conséquent elle doit être lue en avance.

Utiliser le point-virgule ou la virgule est plus permissif car ces deux séparateurs démarrent immédiatement la commande se trouvant juste avant eux. Mais si votre projet réclame des contraintes temporelles fortes, les séparateurs & et | sont là pour vous aider.

Il est possible de regrouper plusieurs commandes en les encadrant avec des accolades et ainsi élaborer des structures temporelles complexes:

{ { x = 4 time:1s | y = 2 speed:0.1 } & z = 0 sin:200ms ampli:4 } | t = 2,

Conseil: En général, pensez à terminer les commandes entrées dans une console (URBILab ou telnet) par une virgule, pour éviter de bloquer la connexion après avoir saisi une commande sans fin.

Affectations conflictuelles

Comme il est possible d'exécuter des commandes en parallèle, il se peut que des conflits apparaissent. Par exemple, que se passerait-il si le code suivant était exécuté ?

x=1 & x=5;

x=5 est une affectation conflictuelle car elle accède à la variable x en même temps que la première affectation. Pour cela, URBI possède plusieurs modes de mélange (blend) pour prendre en charge les éventuels conflits et vous pouvez indiquer le mode souhaité grâce à la propriété blend de la variable. Par exemple:

x->blend = add;

Cela va demander à URBI d'additionner les valeurs numériques de toutes les affectations conflictuelles sur la variable x. Ainsi, le résultat de la commande précédente sera l'affectation de la valeur 6 à la variable x. Il existe également un mode mix qui réalise la moyenne des affectations conflictuelles (le résultat serait alors 3) ainsi qu'un mode queue qui écrase les affectations conflictuelles (le résultat serait alors 5). D'autres modes sont disponibles et décrits dans le document "URBI Language Specification".

Avec URBI, les variables possèdent des propriétés qui peuvent être accédées avec l'opérateur ->. Une propriété n'est pas l'attribut d'un objet. Les propriétés font partie intégrante de la sémantique du langage et par conséquent ne peuvent être redéfinies. Il existe de nombreuses propriétés disponibles comme rangemin, rangemax, speedmax, delta. Elle sont décrites dans le document "URBI Language specification."

Les modes de mélange s'appliquent également aux devices sonores, tels le haut-parleur de l'Aibo et passer alors de mix à queue passera d'une superposition sonore à une succession sonore (les sons, quoiqu'il arrive, sont joués les uns après les autres).

Les modes add et mix s'avèrent très utiles pour superposer des affectations sinusoïdales pour élaborer des mouvement périodiques complexes, en utilisant des transformations de Fourier du signal et en ne retenant que le coefficient le plus significatif.

Variables et propriétés utiles des devices

Pour l'Aibo comme la plupart des robots, vous trouverez les variables de moteurs suivantes utiles (remplacez device par le nom du device):

  • device.load : règle le couple d'une articulation, entre 0 (complètement lâche) et 1 (rigide).

  • device.PGain : règle le gain P d'une articulation du PID associé.

  • device.IGain : règle le gain I d'une articulation du PID associé.

  • device.DGain : règle le gain D d'une articulation du PID associé.

Et voici des propriétés utiles, qui ne sont pas des variables au sens strict (les propriétés font partie de la sémantique du langage), mais vous pouvez les lire et les règler:

  • device->rangemin : valeur minimale du device

  • device->rangemax : valeur maximale du device

  • device->delta : précision du device, utilisé pour les tests flous

  • device->unit : unité du device (pour indication seulement dans URBI 1.0)

  • device->blend : le mode de mélange du device (normal, mix, add, queue, discard ou cancel)

  • device->info : des renseignements sur le device.

Ces propriétés sont en réalité celles de device.val, sachant que l'on considère que les alias sont opérationnels.

Quelques commandes utiles

Voici une brève liste de commandes qui pourraient s'avérer utiles dans vos programmes:

  • reset : réalise un redémarrage logiciel. Utile pour supprimer un paquet de scripts et envoyer une toute nouvelle version.

  • stopall : arrête toutes les commandes de toutes les connexions. Un peu brutal mais bien utile parfois.

  • reboot : redémarre le robot.

  • shutdown : éteint le robot.

  • uservars : affiche la liste des variables utilisateurs.

  • strict : enclenche le contrôle de définition de variable (voir le document "URBI Language Specification").

  • unstrict : annule l'effet de strict.

Chapter 4. Fonctions plus avancées

Désormais, vous êtes capable de lire et de régler des capteurs et des moteurs dans votre robot, exécuter des scripts ou actions complexes et superposer des séquences de mouvements. Cela pourrait suffire pour la plupart des utilisateurs, mais URBI va plus loin en vous proposant l'ensemble des structures algorithmiques disponibles dans les langages de programmation modernes ainsi que de nouvelles adaptées à la robotique.

Le branchement conditionnel et les boucles

Le branchement conditionnel et les boucles disponibles avec le C et le C++ sont présents dans URBI : if ... else, for et while. Les exemples suivants illustrent ces constructions (la commande echo que vous trouverez parmi les exemples affiche tout simplement l'expression en tant que message-système).

if

if réalise un test et exécute la commande associée, à la condition que le test ait réussi:

if (backSensorM > 0) {
       pressed = 1;
       echo "Back sensor pressed";
};

La dernière commande entre accolades ne nécessite pas d'être conclue par un point-virgule comme dans l'exemple précédent. En effet, les points-virgules sont des séparateurs de commandes et non des terminateurs de commande. Vous pouvez conclure par un point-virgule comme en C, mais cela est inutile (cela ajoute une commande vide).

distance = 100;
if (distance < 10)
  echo ("Obstacle detected")
else
  echo ("No obstacle");
[00042854] *** No obstacle

Remarquez qu'il n'y a pas de point-virgule avant else mais qu'il y en a un (ou tout autre séparateur de commande) aprés l'accolade finale.

distance et backSensorM sont deux "device": l'un pour le capteur de distance infra-rouge situé dans la tête de l'Aibo et l'autre pour le capteur du milieu du dos.

while

La construction d'une boucle while est similaire à celle du C:

i=0;
while (i <= 2) {
  i << echo (i);
  i++;
};
[00043477:i] *** 0.000000
[00043516:i] *** 1.000000
[00043536:i] *** 2.000000

for

La construction d'une boucle for est similaire à celle du C:

for (i = 0; i <= 2; i++)
  i << echo (i);
[00022967:i] *** 0.000000
[00022991:i] *** 1.000000
[00023011:i] *** 2.000000

Contrairement au C, URBI possède des constructions temporelles de for: for&, for| et while|. Ces constructions démarreront chaque itération en parallèle (avec &) ou en série (avec |) avec une garantie de contrainte temporelle. Plus de détails dans le document "URBI Language Specification".

Il existe également une construction for in pour énumérer les listes:

for i in [0, 1, 2]
{
  i << echo (i);
};
[00117921:i] *** 0.000000
[00117921:i] *** 1.000000
[00117921:i] *** 2.000000

for in est une exception: même quand il n'y a qu'une seule commande, comme dans l'exemple précédent, vous devez l'encadrer par des accolades.

loop

Pour des raisons pratiques, URBI a ajouté une construction supplémentaire, loop, pour créer des boucles infinies. La syntaxe est la suivante:

loop { ... }

et

loopn (n) { ... }

Les mécanismes de capture d'évènements

at

at fonctionne un peu comme if, à la différence qu'il tourne en permanence en tàche de fond:

distance = 100;
at (distance < 50) echo ("Obstacle détecté");
distance = 25;
[00972675] *** Obstacle appears

La commande echo dans l'exemple précédent s'exécutera dés que le test deviendra vrai, et ce, une seule et unique fois. Pour être plus précis, at déclenche la commande lorsque le test bascule de vrai à faux. Ceci est trés utile pour démarrer une action lorsque une condition est remplie pour réagir à cette condition. Si vous exécutez le code précédent, le message

Obstacle détecté

s'affichera une fois lorsque vous mettrez votre main devant l'Aibo.

onleave quant à lui se rapproche de else et est suivi d'une action qui sera exécutée dés que le test basculera de vrai à faux:

      distance = 100;
at (distance < 50)
  echo ("Obstacle détecté")
onleave
  echo ("Obstacle disparu");
distance = 25;
[00040422] *** Obstacle appears
distance = 200;
[00048007] *** The obstacle is gone

whenever

whenever fonctionne un peu comme while, à la différence qu'il ne termine jamais et tourne en tâche de fond:

whenever (distance < 50)
  echo "Il y a un obstacle";

La commande echo sera exécutée tant que le test restera vrai. Et s'il redevient vrai par la suite, la boucle redémarrera pour, à nouveau, durer tant que le test restera vrai. Comparé à l'exemple précédent, la différence est que le message

Il y a un obstacle

sera affiché de nombreuses fois, aussi longtemps que vous laisserez votre main devant la tête du robot.

Il est possible d'ajouter une construction else pour indiquer une action à réaliser pendant que le test est faux:

whenever (distance < 50)
  echo "Il y a un obstacle"
else
  echo "Il y n'a pas d'obstacle";

whenever et at sont deux constructions fondamentales que vous aurez à utiliser lorsque vous programmerez des réactions et des captures d'évènements sur votre robot.

wait, waituntil

La commande wait (n) attendra pendant n millisecondes avant de s'arrêter. Pratique pour provoquer une pause lors d'une séquence de commandes, typiquement des commandes motrices:

headPan = 0 | wait(1s) | headPan = 90;

La commande waituntil(test) attend que le test devienne vrai. Utile pour synchroniser des programmes parallèles avec une condition donnée.

timeout, stopif, freezeif

La commande timeout (n) cmd exécute la commande cmd et l'arrète ensuite si aprés n millisecondes elle n'a toujours pas fini.

x = 0;
timeout(10s) loop x++;
x;
[00262523] 5000.000000

La commande stopif (test) cmd exécute la commande cmd et l'arrête ensuite si le test devient vrai avant qu'elle n'ait fini. Bien sûr, si la commande est déjà terminée, rien ne se produit.

stopif(distance < 50) robot.walk();

La commande freezeif (test) cmd exécute la commande cmd et la gèle ensuite si le test devient vrai avant qu'elle n'ait fini. La commande est dégelée si le test devient faux à nouveau.

freezeif(!ball.visible) trackball();

Cela peut s'avérer trés utile pour indiquer que certaines portions de code ne tournent que lorsque certaines conditions sont réunies.

Les tests coulés

Les tests utilisés dans les commandes de capture d'évènements comme at, whenever, waituntil, stopif ou freezeif peuvent être complétés de contraintes temporelles, devenant des tests coulés (ou soft tests):

      at (headSensor >0 ~ 2s)
  echo ("Quelquechose s'est posé sur ma tête ...");

Cela signifie que le test doit rester vrai pendant deux secondes pour qu'il devienne vrai aux yeux de la commande at. Vous pouvez spécifier la durée en s ou ms en employant le suffixe approprié et le tout doit être séparé du test par un tilde ~.

Les tests coulés sont utilisables dans toutes les commandes de capture d'événements et ils sont trés utiles en robotique en tant que filtres pour capteurs.

Emettre un événement

La programmation événementielle est pratique et elle est une méthode à privilégier pour programmer un robot. L'idée générale de la programmation événementielle est que certaines commandes émettent des événements et d'autres les capturent et réagissent en conséquence.

Evénements simples

Pour émettre un événement, il y a la commande emit en URBI et vous pouvez utiliser at ou whenever pour le capturer:

at (boom())
  echo ("boom!");
emit boom;
[387274344] *** boom!

L'événement boom est local à la connexion. Si vous souhaitez que l'événement soit visible par une autre connexion, ajoutez-y un préfixe, comme myprefix.boom.

Les événements avec paramètres

Vous pouvez ajouter des paramètres à un événement, comme ceci:

emit monevenement(1, "salut");

Les paramètres peuvent être récupérés lors de la capture:

at (monevenement(x,y))
  echo "capture deux: " + x + " " + y;

at (monevenement(1,x))
  echo "capture un: " + x;

Ici le second at est particulièrement intéressant car il réalise un filtrage sur les paramètres de l'événements en n'acceptant uniquement que les événements dont le premier paramètre est égal à 1:

emit monevenement(1, "salut");
[398730683] *** capture deux: 1 salut
[398730683] *** capture un: salut
emit monevenement(2,15);
[400618686] *** capture deux: 2 15

Durée d'un événement

Un événement possède normalement une durée nulle, il est juste un pic (fonction de Dirac du temps). Cependant, vous pouvez demander à un événement de durer un certain laps de temps, indiqué entre parenthèses comme ceci:

emit(10s) boom;
emit(15h12m) monevenement(1, "salut");

Cela fera une différence entre at et whenever par exemple: whenever bouclera pendant toute la durée de l'événement.

Evénements pulsés: la commande every

Vous pouvez imposer à une commande de s'exécuter à intervalle régulier en utilisant la commande every. L'exemple suivant affiche

salut

toutes les dix minutes:

every (10m) echo "salut";

Une utilisation classique de ceci est d'émettre réguliérement un événement à intervalle régulier:

every (100ms) emit pulsation;

Pour arrêter l'émission, utilisez simplement stop sur la commande every avec l'étiquette appropriée:

monmetronome << every (100ms) emit pulsation;
stop monmetronome;

Etiquettes, drapeaux et contrôle des commandes

Le système d'étiquetage décrit au début de ce tutoriel est en réalité plus qu'une simple identification de messages. Par exemple, vous pouvez arrêter n'importe quelle commande avec la commande stop, depuis n'importe quelle connexion:

maboucle << loop legRF2 = legLF2,
...
stop maboucle;

Vous pouvez aussi geler une commande avec la commande freeze et la dégeler (elle reprendra là où elle en était) avec la commande unfreeze. Il existe aussi la combinaison de commandes block/unblock qui permet d'empêcher de nouvelles exécutions de la commande possédant l'étiquette donnée. Une étiquette peut préfixer un ensemble de commandes entre accolades, comme { ... }, et ainsi être associée à une large portion de code et pas forcément une seule commande.

A la suite de l'étiquette, il est possible d'ajouter un ou plusieurs drapeaux (flags). Un drapeau est un mot-clé préfixé par le signe +. Les drapeaux les plus utiles sont +begin et +end qui envoient un message-système lorsque la commande démarre et lorsqu'elle s'arrête, ou +bg qui place la commande en tàche de fond. Voici quelques exemples illustrant tout ceci:

monetiquette+begin: loop legRF2 = legLF2,
[139464:monetiquette] *** begin
+begin+end: wait(1s);
[00564537] *** begin
[00565537] *** end

D'autres drapeaux sont présentés dans le document "URBI Language Specification".

Depuis URBI 1.0, vous pouvez utiliser des étiquettes hiérarchiques comme monetiquette.sousetiquette. L'avantage de ceci est que vous pouvez arrêter une famille entière basée uniquement sur l'étiquette-mère: l'étiquette précédente peut aussi bien être arrêtée avec un stop monetiquette.sousetiquette qu'avec un stop monetiquette, et vous pouvez ainsi regrouper facilement plusieurs commandes. Une prochaine version incluera également le multi-étiquetage pour accroître encore les possibilités.

Le regroupement d'objets

Une possibilité importante offerte par URBI est de pouvoir regrouper les objets en hiérarchies. Cela se fait grâce à la commande group: group nomdugroupe { objet1, objet2, ...}, par exemple:

group patteavantgauche {legLF1, legLF2, legLF3};
group pattes {patteavantgauche, pattearrieregauche, patteavantdroite, pattearrieredroite};

Figure 4.1. Une hiérarchie typique de device moteurs

Une hiérarchie typique de device moteurs

Cette fonction de regroupement est associée à la notion de broadcasting, qui est utilisée pour différentes choses. L'une d'entre-elles est l'affectation multi-objets : toute affectation est exécutée pour le groupe et, récursivement, passée aux sous-groupes-enfants. En d'autres termes, en utilisant l'exemple précédent, la commande legLF.val = 0 règlera la valeur de legLF1.val, legLF2.val et legLF3.val à zéro (notez que les alias fonctionnent également si vous le souhaitez).

group ab { a, b };
ab.n = 4;
a << a.n, b << b.n;
[00828842:a] 4.000000
[00828842:b] 4.000000

Pour tout robot supporté, il y aura une hiérarchie de regroupement d'objets disponible au départ. Les commandes établissant cette hiérarchie se trouve dans le fichier URBI.INI ou le fichier std.u.

Par exemple, pour aibo, il y a un groupe de l'ensemble des moteurs et un groupe de l'ensemble des lumières. Vous pouvez ainsi régler toutes les lumiéres à une valeur aléatoire avec la commande suivante:

leds = random(2); // l'allias est utilisé ici

Pour s'amuser, vous pouvez tenter: fun: loop leds = random(2), et admirer le résultat.

Il reste plusieurs choses à dire au sujet des groupes et du broadcasting, qui est une fonction trés puissante d'URBI. Nous reviendrons sur le sujet dans le chapitre Les Objets avec URBI.

Définir une fonction

Pour définir une fonction, vous devez utiliser le mot-clé function suivi du nom de votre fonction en notation préfixe.suffixe (ou simplement suffixe pour une fonction locale à la connexion), et les paramètres entre parenthèses (ou des parenthèses vides s'il n'y a aucun paramètre). Vous pouvez utiliser return pour retourner une valeur ou quitter la fonction, comme en C:

function ajouter(x,y) {
  z = x + y;
  return z;
};
function ecrire(x) 
{
  echo (x);
  if (x < 0)
    return
  else
    echo (sqrt(x));
};

Il faut obligatoirement un point-virgule ou un autre séparateur de commande à la fin de la définition de la fonction, puisque que définir une fonction est une commande comme tout autre commande URBI.

Les paramètres sont toujours locaux à l'appel de la fonction. Les variables non globales (i.e. sans préfixe) dans le corps de la fonction sont également locales à l'appel de la fonction. Etudions l'exemple suivant:

a = 4;
b = 5;
function afficher(b) {
  afficher_b << b; // b est local
  var a = b; // crée une variable locale a
  afficher_a << a;
};
display(10); a << a; b << b;
[139464:display_b] 10.000000
[139464:display_a] 10.000000
[139464:a] 4.000000
[139464:b] 5.000000

Une bonne habitude est d'isoler vos fonctions dans un fichier séparé comme mesfonctions.u, et de les charger avec la commande: load("mesfonctions.u"). Cela peut être fait dans le fichier URBI.INI par exemple, ou quand vous en avez réellement besoin.

Pour retirer la définition d'une function, utiliser simplement:

delete mafonction;

Messages d'erreur et messages-système

Lorsqu'une commande URBI échoue, cela envoie un message d'erreur, préfixé par trois points d'exclamation:

impossible << 1 / 0;
[00224686:impossible] !!! 5.15-19: Division par zero
[00224686:impossible] !!! 5.15-19: EXPR evaluation failed

Remarquez que l'étiquette de la commande est utilisée pour le message d'erreur, ce qui s'avère trés pratique pour savoir ce qui a coincé dans un programme complexe.

Les messages d'erreur sont différents des messages-système, préfixés eux par trois étoiles. Un exemple simple est la commande echo avec drapeau +begin et un drapeau +end:

monetiquette+begin+end << echo "salut tout le monde!";
[146711:monetiquette] *** début
[146711:monetiquette] *** salut tout le monde!
[146711:monetiquette] *** fin

Chapter 5. Les objets avec URBI

La programmation orientée objet est intégrée à URBI, avec de nombreuses innovations comme les attributs virtuels et la diffusion (broadcasting). Ce chapitre aborde l'aspect le plus important des objets en URBI. Il peut être ignoré par les programmeurs débutants même si cela n'est en vérité guère compliqué et vraiment instructif.

Définir une classe

Tout comme en C++, on définit une classe en URBI avec le mot-clé class:

class maclasse;

Vous pouvez naturellement définir ce qui se trouvera dans la classe, c'est à dire des variables, des fonctions et des événements:

class maclasse {
  var x;
  var y;
  function f(a,b);
  event signalemoi(s);
};

Il est important de remarquer que, contrairement aux classes C++, maclasse dans l'exemple précédent est également une instance [4] et vous pouvez donc tout à fait affecter des valeurs à maclasse.x et l'utiliser.

Une fonction importante que vous souhaitez certainement définir est init, qui est le constructeur de la classe (ceci est une autre différence avec le C++, le constructeur n'est pas nommé avec le nom de la classe). Cette fonction ne devrait rien retourner ou retourner zéro pour indiquer la réussite de la création de l'instance et tout autre valeur pour indiquer son échec.

Pour définir le corps d'une méthode de classe, faites-le en dehors de la définition de classe, de la manière suivante:

class maclasse {
  var x;
  function init(a);
};

function maclasse.init(a) {
  x = a;
};

Vous pouvez définir une sous-classe (ou une instance, souvenez-vous qu'il n'y a aucune différence), avec une commande new, comme en C++:

masousclasse = new maclasse(42);

Cela va créer une sous-classe et appeler masousclasse.init(42);.

masousclasse hérite de maclasse, par conséquent tous les attributs et les méthodes de maclasse sont aussi disponibles dans masousclasse. Nous allons voir par la suite la question de la définition par défaut et celle de la redéfinition [5].

masousclasse peut hériter de plusieurs classes en appelant new sur ces différentes classes:

masousclasse = new maclasse (42);
masousclasse = new monautreclasse ();

C'est une façon assez spéciale de traiter l'héritage multiple, comparé au C++.

En appelant new sans parenthèses, avec juste le nom de la classe, on exécute le constructeur init sans paramètre:

mysubclass = new myclass; // same as new
myclass();

Si init n'est pas défini, ou si init retourne une valeur synonyme d'erreur (non vide et non nulle), un message d'erreur sera produit et la création avortée.

Les classes peuvent être complétées pendant l'exécution simplement en créant des fonctions ou des attributs se référant à elles. Exemple:

class maclass {
  var x;
};
var maclass.newattribute;
maclass.s = "hello";
...
function maclass.f(a) {
  s = a;
};

Méthodes virtuelles et attributs

En URBI, toutes les méthodes (fonctions de classe) et tous les attributs (variables de classe) sont virtuels, ce qui signifie que si votre classe le redéfinit, il devient sa propre définition, autrement la définition (ou la valeur) de la classe-mère sera utilisée.

Considérons l'exemple suivant:

class maclasse {
  var x;
  function f();
};
function maclasse.f() {
  echo "Je suis dans maclasse";
};

D'où:

sous = new maclasse;
sous.f();
[01130940] *** Je suis dans maclasse

La définition de f est récupérée depuis maclasse. Nous pouvons maintenant la redéfinir:

function sous.f() {
  echo "Je suis dans sous!";
};

Observons la différence:

sous.f();
[01173929] *** Je suis dans sous!

De la même façon, les attributs obtiennent leur valeur de la classe-mère, à moins qu'ils soient redéfinis dans la classe-fille. L'exemple suivant illustre ce cas avec la classe précédente maclasse et les prototypes de sous:

myclass.x = 1;
sub.x;
[01198648] 1.000000
sub.x = 4;
sub.x;
[01210231] 4.000000
myclass.x;
[01214327] 1.000000

Les groupes

Nous avons vu dans les chapitres précédents comment les groupes peuvent être utilisés pour affecter simultanément une valeur à plusieurs variables d'objets. En fait, le mécanisme est bien plus général et est associé au concept de diffusion qui sera défini précisément dans la prochaine section.

Tout d'abord, quelques mots sur les groupes. Nous avons déjà appris que nous pouvions définir des groupes avec la commande group. De la même façon, vous pouvez ajouter un membre à un groupe avec la commande addgroup et en retirer un avec delgroup, ce qui vous permet la gestion dynamique de vos groupes:

group a {a1,a2};
addgroup a {c,d};
delgroup a {a1,d};

Vous pouvez examiner le contenu d'un groupe en invoquant la commande group avec seulement le nom du groupe:

group a { u, v, b };
group a;
[01299618] ["u","v","b"]

Le regroupement de sous-groupes est possible. Dans ce cas, l'évaluation du contenu du groupe retourne la liste des membres terminaux seulement:

group a { u, v, b };
group b { x, y };
group a;
[01359471] ["u","v","x","y"]

L'utilisation typique de cela est l'énumération de devices, comme des moteurs, qui ont été regroupés dans le même groupe:

for m in group a
{
  echo (m);
};
[01426875] *** u
[01426875] *** v
[01426875] *** x
[01426875] *** y

Le constructeur $ retourne la variable dont le nom est la chaine de caractères donnée en paramètre. Dans l'exemple précédent, nous supposons qu'il y a un alias sur .val, autrement il faudrait écrire: $(m+".val")

Maintenant, nous allons voir comment utiliser en pratique les groupes avec la notion de diffusion.

La diffusion

Quand vous exécutez une commande à l'échelle d'un groupe, qui peut être un appel de fonction ou une affectation, la commande sera propagée en parallèle à chaque membre du groupe et à leurs sous-groupes. Cela s'appelle la diffusion (ou broadcast). Cela peut s'appliquer aussi aux classes, puisque vous pouvez définir un groupe contenant chaque instanciation de classe-fille. Une approche habituelle est de nommer le groupe associé à une classe avec le pluriel du nom de la classe, en ajoutant un simple "s".

Tout d'abord, observons comment une affectation est diffusée:

class a;
a1 = new a;
group as { a, a1 };
a1.x = 42;
as.x = 4;
a.x;
[00031479] 4.000000
a1.x;
[00036327] 4.000000

La diffusion fonctionne de manière similaire avec les fonctions:

class a {
  var x;
  function f();
};

function a.f() {
  echo (x);
};
a1 = new a;
group as { a, a1 };
a.x = 1;
a1.x = 2;
as.f();
[00049398] *** 2.000000
[00049398] *** 1.000000

La fonction précédente f est en réalité exécutée de la façon suivante:

a.f() & a1.f();

Diffuser revient donc à dupliquer les commandes en parallèle.

Les sous-groupes sont naturellement parcourus dans le processus.

Les fonctions diffusées peuvent s'avérer très utiles pour exécuter des tâches en parallèle dans un groupe d'objets, sans avoir à utiliser for& ou une construction similaire. La diffusion et l'héritage se complètent mutuellement, ainsi quand la diffusion est achevée, la définition de la fonction peut être recherchée en remontant dans la hiérarchie de classe, comme dans l'exemple suivant:

class a
{
  var x;
  function init(v);
  function f();
};
function a.init(v)
{
  x = v;
};
function a.f()
{
  echo (x);
};
a1 = new a(1);
a2 = new a(2);
a3 = new a(3);
function a1.f() { echo ("Je suis different!"); };
group oneandtwo { a1, a2 };
oneandtwo.f();
[00156447] *** 2.000000
[00156447] *** Je suis different!

La diffusion est clairement un concept nouveau dans les mains des programmeurs. Vous pouvez l'utiliser ou non, mais nous croyons que cela permettra d'obtenir des programmes plus concis, en regroupant les actions logiques en une ligne, au lieu d'utiliser des boucles for ou un concept itératif similaire. Cela renforce également l'idée que certaines actions doivent être exécutées en parallèle sur un groupe d'objets, ce qui peut rend votre code plus sensé.



[4] Cela s'appelle un langage orienté prototype, comme le javascript.

[5] Actuellement, il n'existe pas de notion d'accès privé, public ou protégé. Elles seront intégrées dans URBI 2.0.

Chapter 6. L'exemple de la détection de balle

La meilleure façon d'apprendre un nouveau langage est d'étudier de petits exemples pour voir ce qui peut être fait en pratique. Dans ce tutoriel, nous allons nous focaliser sur le détecteur de balle pour Aibo qui se révèle intéressant car son comportement n'est constitué que de deux états et parce qu'il implique une boucle perception-action qui est typique dans le domaine de la robotique. Nous allons voir comment URBI peut aider à contrôler l'exécution du comportement de façon simple grâce aux étiquettes.

Détection de la balle

Détecter une balle implique un traitement de l'image et ceci ne peut être écrit directement en URBI pour cause de performance. La meilleure façon de produire de tels composants algorithmiques (comme le traitement de l'image ou du son) est d'écrire un composant UObject en C++, Java ou Matlab et de le connecter à URBI. Nous n'allons pas nous attarder pour l'instant sur l'écriture d'un tel composant mais plutôt en utiliser un: l'objet ball.

L'objet ball est directement intégré dans l'Aibo URBI Engine et vous pouvez l'utiliser directement, comme n'importe quel device. Il n'y a pas de variable ball.val mais des variables ball.x et ball.y qui sont égales aux coordonnées de la balle dans l'image, comprises entre -1/2 et 1/2. Lorsqu'une balle est visible, ball.visible vaut 1, ou 0 dans le cas contraire. Il existe également une variable ball.ratio qui donne la proportion de pixels de la balle dans l'image, exprimée en pourcentage. Ces simples variables sont déjà intéressantes pour de nombreuses applications, tout comme nous allons le voir par la suite.

Le programme principal

Le programme de détection de balle est donné en exemple dans le kit de développement de Sony (SDK OPEN-R) et réalise les actions suivantes: lorsqu'une balle se trouve en face du robot, ce dernier va la suivre en bougeant la tête. Autrement, il va la chercher en bougeant la tête en cercles.

Orienter la tête vers la balle peut être écrit très simplement en URBI avec les deux lignes suivantes:

headPan  = headPan  + camera.xfov * ball.x &
headTilt = headTilt + camera.yfov * ball.y;

Cela aura pour effet de bouger en même temps (c'est ce que veut dire le séparateur &) les deux moteurs de tête pan et tilt, d'une quantité proportionnelle aux positions x et y de la balle dans l'image de sa caméra. Les coefficients camera.xfov et camera.yfov viennent du device camera que nous découvrirons dans le chapitre suivant. Ils représentent l'angle x et l'angle y du champ de vision de la caméra de l'Aibo qui sont utilisés pour convertir l'intervalle normalisé [-1/2;1/2] de ball.x et ball.y en degrés.

Pour suivre la balle et non plus s'orienter une fois dans sa direction, nous allons utiliser la commande whenever:

whenever (ball.visible) {
  headPan  = headPan  + camera.xfov * ball.x &
  headTilt = headTilt + camera.yfov * ball.y;
};

Ce programme fait seulement trois lignes et réalise le comportement de suivi de balle que l'on souhaitait. Cependant, avec un Aibo, cela risque d'être trop réactif et conduira à de faibles oscillations de la tête autour de l'orientation de la balle. Pour éviter cela, une technique robotique simple est d'utiliser un coefficient d'atténuation, ball.a, pour limiter la réactivité du système. Par exemple:

ball.a = 0.8;
whenever (ball.visible) {
  headPan  = headPan + ball.a * camera.xfov * ball.x &
  headTilt = headTilt+ ball.a * camera.yfov * ball.y;
};

La prochaine étape est de basculer de ce comportement vers le comportement de recherche quand la balle n'est pas visible. Le comportement de recherche peut être exprimé avec un simple mouvement sinusoïdal sur à la fois headPan et headTilt. Nous utilisons dans l'exemple suivant l'extension de variable 'n [6] qui indique que l'on travaille avec une valeur normalisée de la variable, comprise entre 0 et 1, calculée à partir des propriétés rangemin (la limite inférieure) et rangemax (la limite supérieure). Cela s'avère très pratique d'éviter de contrôler la valeur actuelle d'un device et de le manipuler d'une façon générique:

periode = 10s;
headPan'n  = 0.5 sin:periode ampli:0.5 &
headTilt'n = 0.5 cos:periode ampli:0.5,

Le modificateur cos est identique à sin mais avec un décalage de phase de pi/2. Remarquez comme la valeur centrale 0.5 avec l'amplitude 0.5 permet de couvrir toute l'étendue du device: [0..1].

La commande précédente réalise le mouvement circulaire requis mais lorsque ce comportement est lancé, la position initiale sera atteinte brusquement depuis l'emplacement où se trouvant la tête avant le lancement de la commande. Pour éviter cela, nous pouvons précéder notre commande avec une transition douce d'une seconde vers la position initiale du cercle qui est headPan'n = 0.5 et headTilt'n = 1:

headPan'n  = 0.5 smooth:1s &
headTilt'n = 1   smooth:1s;

Le modificateur smooth est similaire à time, à la différence qu'il ajoute la douceur du tracé d'un "S", au lieu de faire un mouvement linéaire.

Désormais, nous pouvons tout connecter en un seul comportement, en utilisant la capteur d'évenement at comme ciment. Afin d'éviter de basculer du balayage circulaire au suivi de la balle trop souvent, nous ajoutons également un test coulé, et nous utilisons la fonction loadwav pour précharger deux fichiers sonores que l'on affecte au device speaker (le haut-parleur, nous décrirons ce device plus tard) pour jouer un son lorsque la balle est trouvée ou perdue:

// Parameters initialization
ball.a = 0.9;
period = 10s;
trouvee = loadwav("found.wav");
perdue  = loadwav("lost.wav");
// Comportement principal
whenever (ball.visible ~ 100ms) {
  headPan  = headPan  + ball.a * camera.xfov * ball.x &
  headTilt = headTilt + ball.a * camera.yfov * ball.y;
};

at (!ball.visible ~ 100ms)
recherche: {
          { headPan'n  = 0.5 smooth:1s &
            headTilt'n = 1   smooth:1s } |
          { headPan'n  = 0.5 sin:period ampli:0.5 &
            headTilt'n = 0.5 cos:period ampli:0.5 }
           };

at (ball.visible) stop recherche;

// Comportement sonore
at (ball.visible ~ 100ms) speaker = trouvée
onleave speaker = perdue;

Vous pouvez aussi utiliser la construction onleave pour regrouper les deux commandes at (ball.visible), mais vous devez alors employer la commande at&, pour placer la commande recherche en tâche de fond (car c'est une commande sans fin et donc at ne rendrait jamais la main).

Programmer par un graphe de comportement

Le programme précédent fonctionne, est facile à comprendre et à modifier. Pourtant, il est courant en robotique de concevoir les programmes en termes de comportements, exprimés sous forme de machines à états finis, qui sont des graphes d'états reliés entre-eux par des transitions. La figure 6.1 illustre le graphe de comportement du programme de détection de balle, qui est exemple très simple de comportement à deux états.

Figure 6.1. Le graphe de comportement de la détection de balle

Le graphe de comportement de la détection de balle

Les ellipses représentent les états (dans lequels le robot boucle des actions ou une surveillance) et les flèches les transitions, étiquetées par des conditions. Les rectangles associés aux transitions indiquent certaines actions à exécuter lorsque la transition s'effectue.

La meilleure façon de programmer ce genre de graphe de comportement en URBI est d'utiliser une conjonction des fonctions avec les commandes at et stop pour relier le tout. Tout d'abord, définissons les deux fonctions associées aux deux états du programme de détection de balle:

// Etat de suivi
function suis() {
  whenever (ball.visible) {
    headPan  = headPan + ball.a * camera.xfov * ball.x &
    headTilt = headTilt+ ball.a * camera.yfov * ball.y;
  }
};

// Etat de recherche
function cherche() {
  period = 10s;
  {
    headPan'n  = 0.5 smooth:1s &
    headTilt'n = 1   smooth:1s
  } |
  {
    headPan'n  = 0.5 sin:period ampli:0.5 &
    headTilt'n = 0.5 cos:period ampli:0.5
  }
};

Maintenant, nous pouvons simplement relier les états entre-eux en établissant les transitions avec deux commandes at et en terminant l'état précédent avec des commandes stop:

// Transitions
at (ball.visible ~ 100ms) {
  stop recherche;
  speaker = trouvee;
  suivi: suis();
};

at (!ball.visible ~ 100ms) {
  stop suivi;
  speaker = perdue;
  recherche: cherche();
};

L'avantage de ré-écrire le programme de détection de balle en termes de machine à états finis peut ne pas vous apparaître évident pour l'instant car le programme est très simple. Mais, avec des comportements plus riches de dizaines d'états, chacun avec plusieurs transitions, il s'agit de la façon la plus sure de programmer. Cela rend le code modulaire, clair et facile à modifier.

Les machines à états finis sont une excellente façon de décrire les comportements des robots. Elles ne sont pas parfaites mais c'est pour l'instant la technique la plus employée en robotique. URBI est également capable de décrire des architectures subsumées, hiérarchisées ou réactives et bien d'autres paradigmes.

Contrôler l'exécution du comportement

Les possibilités de geler, arrêter et bloquer les commandes avec URBI forment un outil très puissant pour contrôler l'exécution du comportement. Par exemple, si les transitions, exprimées avec une commande at sont préfixées par une étiquette, comme ceci:

transition_de_suivi:
     at (ball.visible ~ 100ms) {
        stop recherche;
        speaker = trouvee;
        suivi: suis();
     };

transition_de_recherche:
     at (!ball.visible ~ 100ms) {
        stop suivi;
        speaker = perdue;
        recherche: cherche();
     };

Il devient très facile de suspendre temporairement ou de ré-activer une transition de la manière suivante:

freeze transition_de_suivi;
...
unfreeze transition_de_suivi;

D'autrepart, il est possible de bloquer l'exécution d'un état, sans pour autant empêcher les transitions vers lui (attendant silencieusement une autre transition qui fera passer le robot à un autre état):

block recherche;
...
unblock recherche;

En utilisant freeze, block et stop, il est simple de modifier les comportements et ré-affecter les priorités en direct, ce qui est très pratique en robotique. Les possibilités sont innombrables car ces réglages peuvent être contrôlés par des évenements ou par d'autres programmes tournant en parallèle, ou même par un programme de contrôle à distance, ou encore une session telnet.



[6] D'autres extensions sont disponibles dans URBI. Les extensions sont de puissants outils pour moduler l'évaluation d'une variable. Consultez le document "URBI Language Specification" pour de plus amples détails.

Chapter 7. Images et sons

Jusqu'ici, nous n'avons cotoyé que des variables numériques, comme headPan.val. Cela n'est bien sûr pas suffisant pour transmettre images et sons. Certains devices, comme camera, micro ou speaker pour l'Aibo, sont des devices binaires. Dans ce cas, la variable device.val n'est pas une valeur numérique mais une valeur binaire.

Lire une valeur binaire

Vous avez certainement déjà tenté d'évaluer l'une de ces variables binaires:

camera;
[139464] BIN 5347 jpeg 208 160
.................5347 bytes.................

micro;
[139464] BIN 2048 wav 2 16000 16 1
.................2048 bytes.................

URBI préfixe chaque donnée binaire d'un en-tête binaire commençant par le mot-clé BIN, suivi de la taille de la donnée (en octets ou bytes) et un mot-clé indiquant le type de la donnée. Certains paramètres optionnels, comme la taille de l'image, la fréquence d'échantillonnage, le status mono ou stéréo d'un son peuvent suivre. Ensuite, après un retour à la ligne, la donnée binaire en elle-même est retournée (affichée ci-dessus comme une série de points ....), ce qui peut embrouiller un client telnet mais pas un client logiciel URBI [7] .

Ce que l'on appelle un client logiciel est un client ou un composant écrit en C++ ou Java, comme décrit dans le chapitre La liburbi en C++. C'est la manière habituelle de gérer les données binaires quand on souhaite traiter un signal avec URBI.

Affecter une valeur binaire

Vous vous en doutez sûrement, affecter une valeur binaire à un device speaker (le haut-parleur) par exemple n'est guère plus complexe que de le lire. Pour jouer un son sur un Aibo, vous pouvez envoyer au serveur une commande comme celle-ci:

speaker = bin 54112 wav 2 16000 16;
..............54112 bytes..............

L'en-tête doit se terminer par un point-virgule, et rien d'autre. Le contenu binaire commence immédiatement après le point-virgule donc nul besoin d'un retour à la ligne supplémentaire.

Bien sûr, une telle affectation binaire ne peut être faite depuis telnet ou URBIRemote, puisque vous souhaitez probablement que le programme envoie le contenu binaire, et que vous n'ayez pas à le saisir vous-même dans le terminal ! (bien qu'il soit possible de faire jouer un son depuis un client telnet).

Ce petit exemple montre une affectation binaire et une lecture binaire en URBI depuis un client telnet. Mais gardez à l'esprit que ceci n'est qu'une démonstration:

mybin = bin 3;ABC
mybin;
[146711] BIN 3
ABC

Vous pouvez ajouter d'autres paramètres après la taille de la donnée binaire, ils seront stockés avec le contenu binaire à l'intérieur de l'en-tête:

mybin = bin 3 hello world 33;ABC
mybin;
[146711] BIN 3 hello world 33
ABC

Ne confondez pas donnée binaire et donnée textuelle (chaîne de caractères). L'exemple précédent est différent de:

mystring = "ABC";
mystring;
[148991] "ABC"

Attributs associés

Habituellement, un objet device binaire dispose de plusieurs attributs. Un exemple classique est le device camera d'un Aibo qui fournit les attributs suivants:

  • camera.shutter : la vitesse d'obturation de la caméra: 1=lente (par défaut), 2=moyenne, 3=rapide

  • camera.gain : le gain de la caméra: 1=lent, 2=moyen, 3=rapide (par défaut)

  • camera.wb : la balance des blancs: 1=intérieur (par défaut), 2=extérieur, 3=fluorescent

  • camera.format : le format de l'image: 0=YCbCr 1=jpeg (par défaut)

  • camera.jpegfactor : le facteur de compression JPEG (de 0 à 100). 80, par défaut.

  • camera.resolution : la résolution de l'image: 0:208x160 (par défaut) 1:104x80 2:52x40

  • camera.reconstruct : reconstruction de l'image haute résolution (lent): 0:non (par défaut) 1:oui

  • camera.width : largeur de l'image

  • camera.height : hauteur de l'image

  • camera.xfov : Angle de vue horizontal, en degrés

  • camera.yfov : Angle de vue vertical, en degrés

Pour le device speaker, en charge de la production sonore de l'Aibo, vous disposez de:

  • speaker.playing : égal à 1 si un son est en cours de lecture, 0 dans le cas contraire

  • speaker.remain : nombre de millisecondes de son à jouer restantes, 0 quand le tampon est vide.

Avec l'objet speaker, il existe également une méthode qui peut être utilisée pour jouer un son directement à partir d'un fichier présent sur la memorystick:

speaker.play("monson.wav");

Autrement, pour éviter d'avoir un accès disque vous ralentissant dans le cas d'accès fréquent, vous pouvez opter pour le stockage en mémoire. Pour cela, utilisez la fonction loadwav:

monbinaire = loadwav("monson.wav");
speaker = monbinaire;

Exemples d'opérations binaires

Avec URBI, vous pouvez ajouter des binaires, comme dans le cas de la concaténation de sons. Par exemple, considérons le programme suivant:

son = bin 0;
timeout(10s) loop son = son + micro;
speaker = son;

Ce code enregistrera dix secondes de son provenant du device micro et les stockera dans la variable son, et les jouera suite à l'affectation au device speaker. Ceci montre à quel point il peut être simple de manipuler les tampons binaires avec URBI pour de simples tâches comme la concaténation.



[7] URBI Remote comprend les en-têtes URBI, affiche les images et lit les sons, en tenant compte du type.

Chapter 8. La liburbi en C++

Qu'est-ce que la liburbi?

Utiliser URBI depuis un client telnet est trop limité. Vous avez besoin d'envoyer des commandes et de recevoir des messages en utilisant le langage de programmation de votre choix ou, de manière plus générale, vous avez besoin d'interfacer URBI avec d'autres langages.

C'est la raison pour laquelle URBI est un Interface Language: il est bien plus qu'un simple protocole car c'est un langage de script complet agissant comme un protocole. Dans la plupart des applications où intervient l'imagerie informatique ou le traitement sonore, URBI est utilisé conjointement avec C++ ou tout autre langage véloce afin d'effectuer la partie algorithmique. URBI est là pour orchestrer vos comportements, vos boucles action/perception et tout autre élement de haut niveau, avec comme source de décisions la sortie du code rapide de C++, Java ou Matlab.

Qu'est-ce que liburbi? Vous pourriez certes programmer une couche TCP/IP pour C++ ou pour votre langage favori mais c'est relativement trivial et serait fait une fois pour toutes. Voilà pourquoi nous avons créé liburbi. Voici les fonctionnalités que l'on est en droit d'attendre:

  • Ouvrir une connexion vers le robot depuis son langage favori (comme C++),

  • Envoyer une commande au robot depuis ce langage,

  • Demander la valeur d'une variable et la recevoir,

  • Recevoir les messages provenant du robot et réagir de manière appropriée.

Le dernier point est très important et, même si cette approche est très différente de votre façon de programmer, il est essentiel de vous adapter à cette logique-ci de programmation (appelée programmation asynchrone) car elle est la plus adaptée à la robotique. Les robots sont fondamentalement des systèmes asynchrones. On attend des messages de la part du robot pour y réagir ensuite. (ceci est également appelé programmation évenementielle). C'est ce qu'un robot fait la plupart du temps: réagir à des évenements [8] .

Ce chapitre est une brève introduction à liburbi. Vous devriez lire la documentation officielle de liburbi sur http://www.gostai.com/docs.php si vous souhaitez une description plus étoffée. Si vous programmez en C++, nous vous conseillons d'utiliser l'architecture UObject décrite plus tard dans ce tutoriel, liburbi n'étant qu'un complément à la nouvelle et prometteuse technologie UObject.

Les composants et liburbi

Etendre URBI avec du code écrit en C++, Java ou Matlab qui sera accessible à vos scripts URBI peut être réalisé de deux façons. La première est d'exploiter la liburbi de votre langage préférée (C++/Java/Matlab) pour construire un client URBI. C'est ce que nous allons décrire dans ce chapitre.

La seconde, plus puissante, est de créer un composant UObject qui est associé à un objet C++. Ceci est expliqué au chapitre Créer des composants: l'architecture UObject. L'objet sera accessible comme tout autre objet URBI, partageant ses méthodes et ses attributs. Il s'agit de la manière la plus portable et la plus flexible d'incorporer des fonctionnalités inédites à URBI. Mais commençons par la traditionnelle liburbi. L'un des aspects séduisants de la liburbi est qu'elle est disponible pour de nombreux langages, bien que le jumelage-objet ne soit pas toujours possible et qu'il soit pour l'instant limité au C++. Aborder la programmation avec la liburbi permet déjà de mener à bien certains projets de programmation et, chose intéressante pour les débutants, de s'initier à l'aspect asynchrone de la robotique.

Il existe actuellement une version C++, une version Matlab, une version Java et une version Python de la liburbi si vous désirez contrôler votre robot dans l'un des langages. Il existe également une version OPEN-R, permettant de recompiler un programme pour le faire tourner uniquement sur l'Aibo. Cependant, nous vous déconseillons cette dernière approche au profit des UObject. La seule raison d'être de cette version OPEN-R est qu'elle offre une gestion implicite du multi-threading non bloquant, indisponible dans le système d'exploitation de base de l'Aibo, Aperios.

Nous ne décrirons pas ici la totalité des implémentations de liburbi mais seulement celle dédiée au C++, permettant cependant de s'introduire aux concepts généraux. Les autres versions sont similaires et possèdent chacune leur documentation. Nous supposons dans ce qui suit que vous disposez d'un minimum de connaissances sur le C++. Dans le cas contraire, consultez un rapide tutoriel C++ pour vous familiariser aux concepts de base de ce langage populaire.

Premiers pas

Pour commencer, il vous faut pouvoir compiler un programme basé sur la liburbi. Il y a plusieurs façons de s'y prendre selon que vous soyez sous Linux ou Windows, avec un compilateur Borland ou Microsoft, etc. D'une manière générale, il suffit d'inclure liburbi.h et de lier votre code avec la liburbi (-lurbi par exemple pour le compilateur gcc). Consultez la documentation appropriée pour de plus amples détails.

Du côté du code, la première chose à faire est de créer un client qui se connectera à votre robot, disons à l'adresse monrobot.mondomaine.com. Pour cela, vous disposez d'une classe UClient:

UClient* client = new UClient("monrobot.mondomaine.com");

Si vous connaissez l'adresse IP, vous pouvez la mettre à la place.

Vous pouvez également appeler explicitement la classe UClient, en utilisant une fonction de l'espace de nommage urbi:

UClient* client = urbi::connect("monrobot.mondomaine.com");

Bien sûr, vous pouvez créer ainsi autant de clients que vous le désirez.

Envoyer une commande

L'objet UClient possède une méthode send qui fonctionne comme printf:

client->send("motor on;");
for (float val=0; val<=1; val+=0.05)
  client->send("neck'n = %f;wait (%d);", val, 50);

Vous pouvez également utiliser votre objet-client comme un flux si vous préférez cette manière typiquement C++:

client << "headPan = " << 12 << ";";

Il existe également une manière très pratique d'envoyer des blocs entiers de code URBI depuis votre programme C++, grâce à la macro URBI((...)):

URBI((
      headPan = 12,
      echo "salut" | speaker.play("test.wav") & leds = 1
    ));

Le texte brut entre les doubles parenthèses sera envoyé directement au premier client créé par votre programme, par défaut. Cela peut se régler en appelant urbi::connect(...). La première possibilité que nous avons abordée, celle employant la méthode send, est plus appropriée dans la plupart des cas et la macro URBI ne devrait servir qu'à envoyer des scripts d'initialisation en début de programme ou pour prototyper.

Gardez à l'esprit que vous pouvez toujours faire repartir votre robot sur des bases neuves (reboot virtuel) en lui envoyant la commande reset. Cela vous évitera les définitions multiples à chaque nouvelle exécution de votre client. Voilà pourquoi de nombreux programmes principaux débutent par client->send("reset;");.

Envoyer une donnée binaire

Pour envoyer une donnée binaire, vous utiliserez la méthode sendBin, au lieu de send:

client->sendBin(soundData, soundDataSize,
                       "speaker = BIN %d raw 2 16000 16 1;",
                       soundDataSize);

Les deux premiers paramètres sont le son lui-même et sa taille. Viens ensuite l'en-tête URBI et enfin les paramètres otpionnels, employant une syntaxe à la prin