Tutoriel

De ZbahWiki.

Sommaire

Ecriture de votre premier programme

Dans ce premier chapitre, nous vous montrerons comment écrire un programme extrêmement simple. Nous allons programmer un robot qui avancera pendant 4 secondes, puis reculera pendant 4 secondes, puis s'arrêtera. Pas très compliqué mais cela introduira les idées de base de la programmation. Et cela vous montrera à quel point c'est facile. Mais avant de commencer à programmer, nous avons besoin d'un robot.

Construction du robot

Le robot que nous allons utiliser tout au long de ce tutoriel est Tribot, le premier robot que vous avez construit lors de l'obtention de la boite du Lego Mindstorm. Ici, la seule différence est que vous devez brancher le moteur droit au port A, le moteur gauche au port C et le dernier moteur au port B.

Assurez-vous d'avoir correctement installé les pilotes Mindstorms NXT fournis avec votre appareil.

Utilisation d'un éditeur

Nous allons utiliser le logiciel libre Gedit afin d'écrire nos programmes. Si vous êtes sous Debian ( ou debian-like ) et qu'il n'est pas installé, il suffit de l'installer via cette commande :

sudo apt-get install gedit

Vous obtiendrez ceci lorsque vous ferez un HelloWorld :

Fichier:Capture-helloWorld.nxc (~-Dropbox-NXC-NXC) - gedit.png

Vous remarquerez en bas à gauche que j'ai sélectionné "C" comme langage de programmation car le NXC n'est pas assez rependu pour pouvoir s'offrir une coloration syntaxique sur ce logiciel. Le NXC est cependant proche du C, vous disposerez donc d'une coloration syntaxique assez propre.

Ecriture du programme

Maintenant, recopiez le programme suivant dans votre éditeur préféré :

task main()
{
    OnFwd(OUT_A, 75);
    OnFwd(OUT_C, 75);
    Wait(4000);
    OnRev(OUT_AC, 75);
    Wait(4000);
    Off(OUT_AC);
}

Au premier coup d'œil, ça a l'air compliqué mais nous allons l'analyser ensemble.

Un programme NXC est composé de tâches. Notre programme n'a qu'une tâche, nommée main. Chaque programme doit avoir une tâche nommée main. Cette tache sera celle exécutée par le robot. Vous en apprendrez plus sur les tâches dans un prochain paragraphe. Une tâche se compose d'un certain nombre de commandes, aussi appelé déclarations. Il y a des accolades autour de ces déclarations pour montrer qu'elles appartiennent bien à la tache main. Chaque déclaration se termine par un point virgule. De cette façon, on voit clairement où finit une déclaration et où en débute une autre. En outre, une tache est généralement définie comme ceci :

task main()
{
    declaration1 ;
    declaration2 ;
    ...
}

Notre programme contient six déclarations. Regardons les une par une.

OnFwd ( OUT_A , 75 );

Cette déclaration demande au robot de démarrer le moteur de la sortie A, qui est le moteur connecté sur le port identifié A sur la brique A, vers l'avant.

OnFwd ( OUT_C , 75 );

Même déclaration que la précédente, pour démarrer le moteur C. Après ces deux déclarations, les deux moteurs sont en fonctionnement et le robot avance.

Wait ( 4000 );

Maintenant il est temps d'attendre un certain temps. Cette déclaration nous dit d'attendre pendant 4 secondes. L'argument, qui est, le nombre ( entre parenthèses ) de millisecondes : vous pouvez donc très précisément indiquer au programme le temps d'attente.Le robot a maintenant assez avancé, on peut le faire se déplacer en sens inverse. Notez que nous pouvons modifier les deux moteurs à la fois en utilisant comme argument OUT_AC. Nous pourrions aussi avoir combiné les deux premiers états de cette façon.

Wait ( 4000 );

Encore, nous attendons 4 secondes.

Off ( OUT_AC );

Et finalement, on éteint les deux moteurs.

C'est la fin du programme. Il avance 4 secondes, recule 4 secondes et éteint les moteurs.

Vous avez probablement remarqué que les couleurs apparaissent automatiquement lors de la saisie dans le programme. Les couleurs et les styles utilisés par l'éditeur quand il effectue la coloration syntaxique sont personnalisables.

Compiler et transférer votre programme

Une fois que vous avez écrit un programme, il doit être compilé (qui est, transformé en code binaire que le robot peut comprendre et exécuter) et envoyé au robot en utilisant le câble USB ou dongle BT (appelé "downloading" du programme).

Sous linux

Après avoir installé le compilateur et le programme de transfert USB, il vous faut compiler le programme.

nbc votre_code.nbc -O=votre_executable.rxe

Puis connectez la brique et transférez le programme.

t2n votre_executable.rxe

Avec Bricx Command Center sous Windows

Comme vous pouvez le voir sur l'image ci-dessous (dans l'encadré rouge) il y a les boutons qui permettent (de gauche a droite) de compiler, de compiler et de télécharger, d'exécuter le programme et de stopper le programme.

Fichier:Compiler.png

Pressez le second bouton, assurez-vous de ne pas avoir fait d'erreurs en tapant votre programme, votre programme sera correctement compilé et téléchargé sur le robot (si vous avez une erreur de compilation, voir la rubrique sur les erreurs).

Maintenant vous pouvez exécuter votre programme. Pour cela, sur le robot, aller dans My Files, Software files et choisissez le nom de votre programme et faite run.

Vous pouvez aussi exécuter le programme avec le raccourci Ctrl+F5, ou après téléchargement du programme vous pouvez cliquer sur le bouton vert (exécuter).

Les erreurs dans vos programmes

Sous linux

Après avoir compilé votre programme, il se peut que le compilateur vous envoi quelques messages dont l'amabilité sera douteuse... Ne vous inquiétez pas, il s'agit surement d'un message d'erreur. Voici un exemple. Prenez le code suivant et compilez le :

task main () {
    OnRev ( OUT_A , 75 );
    OfF ( OUT_A , 75 );
}

Voici la commande pour compiler :

nbc erreur.nxc -O=erreur.rxe

Ensuite, le compilateur vous sortira tout un tas de ligne que l'on expliquera pas ici, puis il vous dira :

# Error: Undefined Identifier OfF
File "/home/NXC/.nxc" ; line 3
#    OfF (
#----------------------------------------------------------
# Error: ';' expected
File "/home/NXC/.nxc" ; line 3
#    OfF ( 0
#----------------------------------------------------------
# Status: NXC compilation failed.

La première erreur vous dit ( clairement ) qu'il ne connait pas l'identifiant ( ici la fonction ) OfF. L'erreur est simple puisqu'il s'agit en réalité de la fonction Off. La deuxième erreur découle simplement de la première. Les erreurs ne sont pas aussi explicite que sous windows ( comme vous le verrez juste après ) mais cela suffit à trouver vos erreurs

Avec Bricx Command Center sous Windows

Lors de la saisie dans les programmes il y a une chance raisonnable que vous aillez fait des erreurs. Le compilateur va vous indiquer les erreurs et les reportes dans le bas de la fenêtre, comme vous pouvez le voir ci-dessous.

Fichier:Erreur.png

Il sélectionne automatiquement la première erreur. Quand il y a plus d'erreurs, vous pouvez cliquez sur les messages d'erreur afin d'aller vers eux. Notez que souvent des erreurs au début du programme cause d'autres erreurs à d'autres endroits. Il vaudrait donc mieux corriger les premières erreurs, puis compiler le programme à nouveau. A noter également que la coloration syntaxique aide beaucoup à éviter les erreurs. Par exemple, sur la dernière ligne nous avons tapé Of au lieu de Off. Comme il s'agit d'une commande inconnue, il n'est pas mis en évidence.

Il y a aussi des erreurs qui ne sont pas trouvées par le compilateur. Si vous actionnoez le mauvais moteur votre robot présentera un comportement inattendu, il est le plus probable que vous aillez fait une erreur dans votre programme.

Changer la vitesse

Comme vous avez remarqué, le robot avance plutôt rapidement. Pour modifier la vitesse il suffit de modifier le deuxième paramètre à l'intérieur des parenthèses. La puissance est un nombre entre 0 et 100. 100 est le plus rapide, 0 pour arrêter (servo-moteurs NXT tiendra position). Voici une nouvelle version de notre programme dans lequel le robot se déplace lentement:

task main()
{
    OnFwd(OUT_AC, 30);
    Wait(4000);
    OnRev(OUT_AC, 30);
    Wait(4000);
    Off(OUT_AC);
}

Conclusion

Dans ce chapitre, vous avez écrit votre premier programme en NXC, en utilisant ou non BricxCC. Vous devriez maintenant savoir comment écrire un programme, comment le télécharger dans le robot et comment faire exécuter le programme par le robot. BricxCC peut faire beaucoup plus de choses. Pour en savoir plus sur eux, lisez la documentation qui l'accompagne. Ce tutoriel portera principalement sur le langage NXC et ne mentionnent que les caractéristiques de BricxCC dont vous avez vraiment besoin.

Un programme plus intéressant

Notre premier programme n'était pas si extraordinaire. Essayons donc de le rendre plus intéressant. Nous allons dans un certain nombre d'étapes, introduire de nombreuses caractéristiques importantes de notre langage de programmation NXC.

Faire tourner

Vous pouvez faire tourner votre robot en arrêtant ou en inversant le sens de l'un des deux moteurs. Voici un exemple. Compilez-le, télécharger-le sur votre robot et faites-le fonctionner. Il doit avancer un peu et puis tourner à droite à 90 degrés.

task main()
{
    OnFwd(OUT_AC, 75);
    Wait(800);
    OnRev(OUT_C, 75);
    Wait(360);
    Off(OUT_AC);
}

Vous pourriez avoir à essayer des valeurs légèrement différentes de 500 dans la seconde commande Wait () pour faire un virage à 90 degrés. Cela dépend du type de surface sur laquelle le robot avance. Plutôt que de changer cela dans le programme, il est plus facile de créer une constante pour cette valeur. En NXC vous pouvez définir des valeurs constantes comme le montre le programme suivant.

#define MOVE_TIME   1000
#define TURN_TIME    360
task main()
{
    OnFwd ( OUT_AC , 75 );
    Wait ( MOVE_TIME );
    OnRev ( OUT_C , 75 );
    Wait ( TURN_TIME );
    Off ( OUT_AC );
}

Les deux premières lignes définissent deux constantes. Celles-ci peuvent maintenant être utilisées dans le programme. Définir des constantes est bon pour deux raisons: il rend le programme plus lisible, et il est plus facile de changer les valeurs.

Les commandes répétitives

Essayons maintenant d'écrire un programme qui fait avancer le robot dans un carré. Avancer dans une carré signifie : marche avant, tournant à 90 degrés, en avançant de nouveau, tournant à 90 degrés, etc Nous pourrions répéter la partie de code du dessus à quatre reprises, mais cela peut être fait beaucoup plus facilement avec l'instruction repeat.

#define MOVE_TIME     500
#define TURN_TIME     500
 
task main()
{
    repeat(4)
    {
        OnFwd(OUT_AC, 75);
        Wait(MOVE_TIME);
        OnRev(OUT_C, 75);
        Wait(TURN_TIME);
    }
    Off(OUT_AC);
}

Le nombre entre parenthèses de l'instruction repeat indique combien de fois le code à l'intérieur des accolades doit être répété. Notez que, dans le programme ci-dessus, nous avons également des déclarations indentées. Ce n'est pas nécessaire, mais il rend le programme plus lisible.

Comme dernier exemple, faisons faire 10 fois le tour au robot.

#define MOVE_TIME     500
#define TURN_TIME     500
 
task main()
{
    repeat(10)
    {
        repeat(4)
        {
            OnFwd(OUT_AC, 75);
            Wait(MOVE_TIME);
            OnRev(OUT_C, 75);
            Wait(TURN_TIME);
        }
    }
    Off(OUT_AC);
}

Il ya maintenant une déclaration repeat dans l'autre repeat. Nous appelons cela une déclaration repeat "imbriquée". Vous pouvez imbriquer des déclarations repeat autant de fois que vous le souhaitez. Gardez un regard attentif sur les accolades et l'indentation du programme. La tâche commence à la première accolade et se termine à la dernière. La déclaration du premier repeat commence à la deuxième accolade et se termine à la cinquième. L'instruction repeat imbriquée commence à la troisième accolade et se termine à la quatrième. Comme vous le voyez les accolades viennent toujours par paires et le code entre les accolades est indenté.

Ajouter des commentaires

Pour que votre programme soit encore plus lisible, il est bon d'y ajouter quelques commentaires. Chaque fois que vous mettez // sur une ligne, le reste de cette ligne est ignoré et peut être utilisé pour les commentaires. Un long commentaire peut être mis entre / * et * /. Les commentaires sont colorés syntaxiquement dans BricxCC. Le programme complet peut se présenter comme suit:

/*   10 SQUARES
Ce programme fait faire 10 carrés au robot
*/
#define MOVE_TIME    500      // Temps pour avancer
#define TURN_TIME    360      // Temps pour tourner à 90 degrés
task main()
{
   repeat(10)                 // Fait 10 carrés
   {
       repeat(4)
       {
           OnFwd(OUT_AC, 75);
           Wait(MOVE_TIME);
           OnRev(OUT_C, 75);
           Wait(TURN_TIME);
       }
   }
   Off(OUT_AC);          // Éteint les moteurs
}

Conclusion

Dans ce chapitre, vous avez appris l'utilisation de la déclaration repeat et l'utilisation des commentaires. Aussi vous avez vu l'utilisation des accolades imbriquées et l'indentation. Avec tout ce que vous savez, vous pouvez faire bouger le robot le long de toutes sortes de chemins. C'est un bon exercice d'essayer d'écrire des variantes des programmes de ce chapitre avant de poursuivre avec le prochain chapitre.

Maintenant que vous êtes à la fin de ce chapitre, vous avez toutes les compétences dont vous avez besoin pour faire faire à votre robot des choses compliquées. Les autres chapitres de ce tutoriel vous apprendront des choses qui ne sont importantes que dans certaines applications.

Utilisation des variables

Les variables forment un aspect très important de tout langage de programmation. Les variables sont des emplacements de mémoire dans lesquels on peut stocker des valeurs. Nous pouvons utiliser ces valeurs à différents endroits et nous pouvons les changer. Nous allons décrire l'utilisation des variables en utilisant un exemple.

#define TURN_TIME    360
int move_time;           // définition d'une variable
task main()
{
    move_time = 200;
    repeat(50)
    {
        OnFwd(OUT_AC, 75);
        Wait(move_time);
        OnRev(OUT_C, 75);
        Wait(TURN_TIME);
        move_time += 200;
    }
    Off(OUT_AC);
}

Les lignes intéressantes sont indiquées avec des commentaires. Nous avons d'abord définit une variable en tapant la mot-clé int suivi d'un nom que nous choisissons. (Normalement, nous utilisons des lettres minuscules pour les noms de variables et les lettres majuscules pour constantes.) Le nom doit commencer par une lettre, mais peut contenir des chiffres et des traits de soulignement. Aucun autre symbole n'est autorisé. (de même pour les constantes, les noms des tâches, etc) Le mot int désigne un entier. Seuls les nombres entiers peuvent être stockés. Dans la deuxième ligne on assigne la valeur 200 à la variable. À partir de ce moment, lorsque vous utilisez la variable, sa valeur sera de 200. Suit maintenant la boucle dans laquelle nous utilisons la variable pour indiquer la temps à attendre et, à la fin de la boucle nous augmentons la valeur de la variable de 200. Donc la première fois le robot attend 200 ms, la deuxième fois 400 ms, pour la troisième fois 600 ms, et ainsi de suite.

Avancer dans un cercle

Outre l'ajout de valeurs à une variable, nous pouvons aussi multiplier une variable avec un numéro à l'aide *=, soustraire à l'aide -= et diviser en utilisant /=. (Notez que pour la division le résultat est arrondi à l'entier le plus proche.) Vous pouvez également ajouter une variable à l'autre, et écrire des expressions plus complexes. L'exemple suivant n'a pas d'effet sur ​​le robot, car nous ne savons pas comment utiliser l'écran du NXT encore!

int aaa;
int bbb,ccc;
int values[];
task main()
{
    aaa = 10;
    bbb = 20 * 5;
    ccc = bbb;
    ccc /= aaa;
    ccc -= 5;
    aaa = 10 * (ccc + 3); // aaa vaut maintenant 80
    ArrayInit(values, 0, 10); // on alloue 10 éléments = 0
    values[0] = aaa;
    values[1] = bbb;
    values[2] = aaa*bbb;
    values[3] = ccc;
}

Notez sur les deux premières lignes que nous pouvons définir plusieurs variables en une seule ligne. Nous pourrions aussi avoir réuni les trois variables sur une seule ligne. La variable nommée values est un tableau, c'est une variable qui contient plus d'un nombre: un tableau peut être indexé avec un certain nombre entre les crochets. En NXC, les tableaux d'entiers sont déclarés ainsi:

int name[];

Ensuite, cette ligne alloue 10 éléments qui sont initialisés à 0.

ArrayInit(values, 0, 10);

Nombres aléatoires

Dans tous les programmes ci-dessus nous avons défini exactement ce que le robot était censé faire. Mais les choses deviennent beaucoup plus intéressantes quand le robot va faire des choses que nous ne savons pas. Nous voulons un peu de hasard dans les mouvements. En NXC vous pouvez créer des nombres aléatoires. Le programme suivant utilise cette technique pour faire tourner robot d'une manière aléatoire. Il avance constamment vers l'avant pour un temps aléatoire, puis tourne au hasard.

int move_time, turn_time;
task main()
{
    while(true)
    {
        move_time = Random(600);
        turn_time = Random(400);
        OnFwd(OUT_AC, 75);
        Wait(move_time);
        OnRev(OUT_A, 75);
        Wait(turn_time);
    }
}

Le programme définit deux variables, puis assigne des nombres aléatoires pour eux. Random (600) renvoi un nombre aléatoire compris entre 0 et 600 (la valeur maximale est pas exclue). Chaque fois que vous l'appelez, les numéros choisis au hasard seront différents.

Notez que nous pourrions éviter l'utilisation des variables en écrivant par exemple :

Wait (Random (600)).

Vous voyez également un nouveau type de boucle ici. Plutôt que l'utilisation de l'instruction repeat, nous avons écrit while (true). L'instruction while répète les déclarations en-dessous aussi longtemps que les conditions entre parenthèses sont vraies. Le mot spécial true est toujours vrai, alors les états entre les accolades sont répétés pour toujours (ou du moins jusqu'à ce que vous appuyez sur le bouton gris foncé du NXT). Vous en apprendrez plus sur les déclarations plus tard.

Conclusion

Dans ce chapitre, vous avez appris l'utilisation des variables et des tableaux. Vous pouvez déclarer d'autres types de données qu'un int: short, long, byte, bool et string. Vous avez également appris à créer des nombres aléatoires, de sorte que vous pouvez donner au robot un comportement imprévisible. Enfin nous avons vu les utilisations de l'instruction while pour faire une boucle infinie.

Les structures de contrôle

Dans les chapitres précédents nous avons vu les déclarations repeat et while. Ces déclarations contrôlent la façon dont les autres déclarations sont exécutées. Ils sont appelés "structures de contrôle". Dans ce chapitre, nous allons voir quelques autres structures de contrôle.

La déclaration if

Parfois, vous voulez qu'une partie de votre programme soit exécutée uniquement dans certaines situations. Dans ce cas, l'instruction if est utilisée. Permettez-moi de vous donner un exemple. Nous allons à nouveau changer le programme avec lequel nous travaillons depuis le début, mais avec une nouvelle tournure. Nous voulons que le robot avance le long d'une ligne droite et tourne, soit à gauche ou à droite. Pour ce faire nous aurons encore besoin de nombres aléatoires. Nous choisissons un nombre aléatoire qui est soit positif, soit négatif. Si le nombre est négatif, le virage sera vers la droite, sinon vers la gauche. Voici le programme:

#define MOVE_TIME    500
#define TURN_TIME    360
task main()
{
    while(true)
    {
        OnFwd(OUT_AC, 75);
        Wait(MOVE_TIME);
        if (Random() >= 0)
        {
            OnRev(OUT_C, 75);
        }
        else
        {
            OnRev(OUT_A, 75);
        }
        Wait(TURN_TIME);
    }
}

L'instruction if ressemble un peu à l'instruction while. Si la condition entre parenthèses est vraie alors la partie entre accolades est exécutée. Sinon, la partie entre accolades après le mot else est exécutée. Regardons de plus prêt la condition que nous utilisons. On a Random() >= 0. Cela signifie que Random () doit être plus grand ou égal à 0 pour que la condition soit vraie. Vous pouvez comparer les valeurs de différentes manières. Voici les plus importantes:

== // Égal
<  // Plus petit que
<= // Plus petit ou égal
>  // Plus grand que
>= // plus grand ou égal
!= // Différent

Il est possible de combiner les conditions avec && pour et ou || pour ou. Voici quelques exemples :

true             // Toujours vrai
false            // Toujours faux
t != 3           // t différent de 3
t > 5 && t < 10  // t compris entre 5 et 10

Notez que l'instruction if a deux parties. La partie immédiatement après la condition​​, est exécutée lorsque la condition est vraie, et la partie après le else, qui est exécutée lorsque la condition est fausse. Le mot-clé else et la partie qui suit sont facultatives. Donc vous pouvez les omettre si il n'y a rien à faire quand la condition est fausse.

La déclaration do

Il existe une autre structure de contrôle : l'instruction do. Elle a la forme suivante :

do
{
   statements;
}
while (condition);

Les instructions entre accolades après l'instruction do sont exécutées tant que la condition entre parenthèses est vraie. La condition ​​a la même forme que dans l'instruction if décrite ci-dessus. Voici un exemple de programme. Le robot se promène au hasard pendant 20 secondes puis s'arrête.

int move_time, turn_time, total_time;
task main()
{
    total_time = 0;
    do
    {
        move_time = Random(1000);
        turn_time = Random(1000);
        OnFwd(OUT_AC, 75);
        Wait(move_time);
        OnRev(OUT_C, 75);
        Wait(turn_time);
        total_time += move_time;
        total_time += turn_time;
    }
    while (total_time < 20000);
    Off(OUT_AC);
}

Notez également que l'instruction do est presque la même que l'instruction while. Mais dans avec l'instruction while la condition est testée avant l'exécution des déclarations, alors qu'avec l'instruction do la condition est testée à la fin. Pour l'instruction while, les déclarations pourraient ne jamais être exécuté, mais pour l'instruction do elles sont exécutées au moins une fois.

Conclusion

Dans ce chapitre, nous avons vu deux nouvelles structures de contrôle: l'instruction if et l'instruction while. Il est très important que vous compreniez ce qu'elles font. Il vaudrait donc mieux essayer d'autres exemples avant de continuer.

Les capteurs

Bien sûr vous pouvez connecter des capteurs à la brique NXT pour permettre au robot de réagir avec les évènements extérieurs. Avant que l'on puisse voir comment faire cela, nous devons changer le robot un peu en ajoutant un capteur tactile. Comme auparavant, suivez les instructions Tribot pour construire le pare-chocs avant. Connectez le capteur tactile à l'entrée 1 de la brique NXT.

Attendre après un capteur

Commençons avec un programme très simple dans lequel le robot avance jusqu'à ce qu'il heurte quelque chose. Voici le programme :

task main()
{
    SetSensor(IN_1,SENSOR_TOUCH);
    OnFwd(OUT_AC, 75);
    until (SENSOR_1 == 1);
    Off(OUT_AC);
}

Il ya deux lignes importantes ici. La première ligne du programme indique au robot le type de capteur que nous utilisons. IN_1 est le numéro de l'entrée à laquelle nous avons connecté le capteur. Les entrées de capteur sont appelées IN_2, IN_3 et IN_4. SENSOR_TOUCH indique qu'il s'agit d'un capteur tactile. Pour le capteur de lumière que nous utiliserions SENSOR_LIGHT. Après avoir précisé le type de capteur, le programme se met en marche. Les deux moteurs et le robot commencent à se déplacer vers l'avant. La déclaration suivante possède une construction très utile. Il attend que la condition entre parenthèses soit vraie. Cette condition indique que la valeur du capteur SENSOR_1 doit être de 1, ce qui signifie que le capteur est pressé. Tant que le capteur n'est pas enfoncé, la valeur est 0. Donc, cette déclaration attend jusqu'à ce que le capteur soit enfoncé. Ensuite, on éteint les moteurs et la tâche est terminée.


Action du capteur tactile

Essayons maintenant de faire éviter les obstacles au robot. Dès qu'il heurtera un objet, il reculera un peu, tournera et continuera sa course. Voici le programme :

task main()
{
    SetSensorTouch(IN_1);
    OnFwd(OUT_AC, 75);
    while (true)
    {
        if (SENSOR_1 == 1)
        {
            OnRev(OUT_AC, 75); Wait(300);
            OnFwd(OUT_A, 75); Wait(300);
            OnFwd(OUT_AC, 75);
        }
    }
}

Comme dans l'exemple précédent, on indique en premier le type de capteur. Ensuite, le robot commence à avancer. Dans la boucle infinie, le robot testera constamment si le capteur est enfoncé et, si c'est le cas, reculera pendant 300ms, tournera à droite pendant 300ms et avancera à nouveau.

Le capteur de couleur

Outre le capteur tactile, vous possédez un capteur de lumière, un capteur sonore et un capteur numérique à ultrasons. Le capteur de lumière peut être déclenchée afin d'émettre de la lumière ou non, vous permettant de mesurer la quantité de lumière ambiante ou la lumière réfléchie dans une direction particulière. Mesurer la lumière réfléchie est particulièrement utile lors d'un robot de suivre une ligne au sol. C'est ce que nous allons faire dans l'exemple suivant. Pour continuer avec les expériences, il nous faut achever la construction Tribot. Branchez le capteur de lumière sur l'entrée 3, le capteur de sons sur l'entrée 2 et le capteur à ultrasons sur l'entrée 4, comme indiqué dans les instructions.


Nous avons également besoin de la piste noire qui vient avec l'ensemble des pièces du NXT. Le principe de base du suiveur de ligne est d'essayer de rester sur la ligne noire. Se détourner de la ligne si le niveau de luminosité est trop faible (et le capteur est au milieu de la ligne) et se tourner vers la ligne si le capteur est hors de la piste ( et détecte une forte luminosité ). Voici un programme très simple avec une unique valeur seuil de lumière.

#define THRESHOLD 40
task main()
{
    SetSensorLight(IN_3);
    OnFwd(OUT_AC, 75);
    while (true)
    {
        if (Sensor(IN_3) > THRESHOLD)
        {
            OnRev(OUT_C, 75);
            Wait(100);
            until(Sensor(IN_3) <= THRESHOLD);
            OnFwd(OUT_AC, 75);
        }
    }
}

Le programme commence par configurer le port 3, un capteur de lumière. Ensuite, il fait se déplacer le robot vers l'avant et entre dans une boucle infinie. Chaque fois que la valeur de la lumière est plus grande que 40 (nous utilisons ici une constante de telle sorte que cela peut être facilement modifiée, car elle dépend beaucoup de la lumière ambiante) on inverse un moteur et on attend d'être sur la bonne voie.

Comme vous le verrez lorsque vous exécuterez le programme, l'execution n'est pas très lisse. Essayez d'ajouter wait(100) avant la commande until pour faire que le robot bouge mieux. Notez que le programme ne permet pas d'aller dans le sens antihoraire. Pour pouvoir se déplacer en suivant un chemin plus long, il nous faudra un programme plus complexe. Pour lire l'intensité lumineuse ambiante avec les DELs éteintes, configurer le capteur comme suit :

SetSensorType(IN_3,IN_TYPE_LIGHT_INACTIVE);
SetSensorMode(IN_3,IN_MODE_PCTFULLSCALE);
ResetSensor(IN_3);


Le capteur sonore

Nous allons écrire un programme qui attend un grand bruit, et fait avancer le robot jusqu'à ce qu'un autre son soit détecté. Branchez le capteur de sons au port 2, tel que décrit dans les instructions du Tribot.

#define THRESHOLD 40
#define MIC SENSOR_2
task main()
{
  SetSensorSound(IN_2);
  while(true){
     until(MIC > THRESHOLD);
     OnFwd(OUT_AC, 75);
     Wait(300);
     until(MIC > THRESHOLD);
     Off(OUT_AC);
     Wait(300);
  }
}

Nous avons d'abord définir constante THRESHOLD et un alias pour SENSOR_2; dans la tâche principale, il faut configurer le port 2 pour pouvoir lire les données du capteur de sons et ensuite nous entrons dans une boucle infinie. En utilisant l'instruction until, le programme attend que le niveau sonore soit supérieur au seuil que nous avons choisi: à noter que SENSOR_2 n'est pas seulement un nom, mais une macro qui retourne la valeur de son lu à partir du capteur. Si un grand bruit se produit, le robot commence à aller tout droit jusqu'à un autre bruit l'arrête. Les états d'attente ont été insérés pour éviter que le robot ne démarre ou s'arrête instantanément: en effet, le NXT est tellement rapide qu'il ne prend pas de temps à exécuter les lignes entre les until. Si vous commentez le premier et la seconde wait, vous pourrez mieux le comprendre. Une alternative à l'utilisation du wait pour attendre les évènements est la boucle while, il suffit de mettre à l'intérieur des parenthèses une condition toujours complémentaire, par exemple :

while(MIC <= THRESHOLD)

Il n'y a pas grand chose d'autre à savoir sur les capteurs analogiques NXT, il suffit de se rappeler que les capteurs de lumière et du son vous renvoi une valeur entre 0 et 100.


Le capteur à ultrasons

Le capteur à ultrasons fonctionne comme un sonar: grosso modo, il envoie une rafale d'ondes ultrasonores et mesure le temps nécessaire pour que les vagues soient réfléchie par l'objet en face. Il s'agit d'un capteur numérique, ce qui signifie qu'il contient un appareil intégrée pour analyser et transmettre des données. Avec ce nouveau capteur, vous pouvez créer un robot capable de voir et d'éviter un obstacle avant de l'atteindre (comme pour les capteurs de contact).


#define NEAR 15 //cm
task main()
{
    SetSensorLowspeed(IN_4);
    while(true){
        OnFwd(OUT_AC,50);
        while(SensorUS(IN_4)>NEAR);
        Off(OUT_AC);
        OnRev(OUT_C,100);
        Wait(800);
    }
}

Le programme initialise le port 4 pour lire les données du capteur à ultra son ; puis exécute toujours une boucle où les robots va tout droit jusqu'à ce que quelque chose de plus proche que NEAR cm (15 cm dans notre exemple) soit en vue, il tournera un peu et recommencera à avancer.

Conclusion

Dans ce chapitre, vous avez vu comment travailler avec tous les capteurs inclus dans la boite du NXT. Nous avons également vu comment les commandes while et until sont utiles pour l'utilisation de capteurs. Je vous recommande d'écrire un certain nombre de programmes par vous-même à ce point. Vous avez tous les ingrédients pour donner à vos robots un comportement assez compliqué t: essayer de traduire en NXC le plus simplement possible les programmes du logiciel du CD-ROM.


Les taches et les sous-routines

Jusqu'à maintenant, tous nos programmes se composait d'une seule tâche. Mais les programmes NXC peut avoir de multiples tâches. Il est également possible de mettre des morceaux de code dans les sous-routines que vous pouvez utiliser dans différents endroits de votre programme. L'utilisation de tâches et de sous-routines rend vos programmes plus faciles à comprendre et plus compact. Dans ce chapitre, nous allons examiner les différentes possibilités.

Les taches

Un programme NXC se compose de 255 tâches au plus, chacune d'entre elles a un nom unique. La tâche nommée main doit toujours exister, puisque c'est la première tâche exécutée. Les autres tâches ne seront exécutées que si une tâche en cours d'exécution leur dit d'être exécutée ou si elles sont explicitement appelées dans la principale; la tâche principale doit se terminer avant que les autres puissent commencer. À partir de ce moment, les deux tâches sont exécutées simultanément.

Permettez-moi de vous montrer l'utilisation des tâches. Nous voulons faire un programme dans lequel le robot avance de façon circulaire. Mais lorsqu'il rencontre un obstacle, il doit réagir. Il est difficile de faire cela en une seule tâche, parce que le robot doit faire deux choses en même temps : avancer en rond et de surveiller les capteurs. Il est donc préférable d'utiliser deux tâches pour cela, une tâche pour qu'il se déplace, l'autre pour réagir avec les capteurs. Voici le programme.

mutex moveMutex;
task move_square()
{
  while (true)
  {
    Acquire(moveMutex);
    OnFwd(OUT_AC, 75); Wait(1000);
    OnRev(OUT_C, 75); Wait(500);
    Release(moveMutex);
  }
}
task check_sensors()
{
  while (true)
  {
    if (SENSOR_1 == 1)
    {
      Acquire(moveMutex);
      OnRev(OUT_AC, 75); Wait(500);
      OnFwd(OUT_A, 75); Wait(500);
      Release(moveMutex);
    }
  }
}
task main()
{
  Precedes(move_square, check_sensors);
  SetSensorTouch(IN_1);
}

La tâche principale définit simplement les types de capteur et planifie le démarrage des deux autres tâches en les ajoutant à la file d'attente du planificateur; après cela, la tâche principale se termine. La tâche move_square fait se déplacer le robot dans un carré indéfiniment. La tâche check_sensors vérifie si le capteur tactile est poussée et, le cas échéant, éloigne robot de l'obstacle. Il est très important de se rappeler que les deux tâches s'exécutent au même moment et cela peut conduire à des résultats inattendus, si les deux tâches essayent d'utiliser les moteurs comme ils sont censés le faire.

Pour éviter ces problèmes, nous avons déclaré un type étrange de variable, mutex (mutual exclusion) : nous ne pouvons agir sur ce type de variables qu'avec les fonction Acquire et Release.Puis nous écrivons des morceaux de code critique entre ces fonctions, assurant que seuls une tâche à la fois peut avoir un contrôle total sur les moteurs. Ces variables de type mutex sont appelés sémaphores et cette technique de programmation est nommé programmation concurrente; cet argument est décrite en détail dans le chapitre 10.

Les sous-routines

Il arrive parfois que vous ayez besoin d'un même code à de multiples endroits dans votre programme. Dans ce cas, vous pouvez alors mettre ce code dans une "sous-routine" et lui donner un nom. Maintenant, vous pouvez exécuter cette partie de code en appelant simplement son nom dans une tâche. Regardons sur un exemple.

sub turn_around(int pwr)
{
  OnRev(OUT_C, pwr); Wait(900);
  OnFwd(OUT_AC, pwr);
}
task main()
{
  OnFwd(OUT_AC, 75);
  Wait(1000);
  turn_around(75);
  Wait(2000);
  turn_around(75);
  Wait(1000);
  turn_around(75);
  Off(OUT_AC);
}

Dans ce programme nous avons défini une sous-routine qui fait tourner le robot autour de son centre. La tâche main appelle la sous-routine 3 fois. Notez que nous l'appelons en écrivant son nom et en lui passant un argument numérique à l'intérieur des parenthèses. Si la sous-routine n'accepte pas d'arguments, il suffit de mettre les parenthèses avec rien dedans. Ça ressemble beaucoup aux commandes que nous avons déjà vu. Le principal avantage des sous-routines est qu'elles ne sont stockées qu'une seule fois dans le NXT et cela libère de la mémoire. Mais lorsque la sous-routine est petite, il peut être préférable d'utilisé les fonction inline à la place. Celles ci ne sont pas copiées séparément mais copiées à chaque endroit où elles sont utilisées. Cela utilise plus de mémoire mais il n'y a pas de limite au nombre de fonctions inline. Elles peuvent être déclarées comme ceci :

inline int Name( Args ) {
   //body;
   return x*y;
}

Définir et appeler des fonctions inline se fait comme pour les sous-routines. Dans l'exemple suivant, nous utilisons les fonction inline :


inline void turn_around()
{
  OnRev(OUT_C, 75); Wait(900);
  OnFwd(OUT_AC, 75);
}
task main()
{
  OnFwd(OUT_AC, 75);
  Wait(1000);
  turn_around();
  Wait(2000);
  turn_around();
  Wait(1000);
  turn_around();
  Off(OUT_AC);
}

On peu même rajouter des arguments :

inline void turn_around(int pwr, int turntime)
{
  OnRev(OUT_C, pwr);
  Wait(turntime);
  OnFwd(OUT_AC, pwr);
}
task main()
{
  OnFwd(OUT_AC, 75);
  Wait(1000);
  turn_around(75, 2000);
  Wait(2000);
  turn_around(75, 500);
  Wait(1000);
  turn_around(75, 3000);
  Off(OUT_AC);
}

Notez que dans les parenthèses derrière le nom de la fonction inline nous spécifions ses arguments. Dans ce cas, nous indiquont que l'argument est un nombre entier (il ya d'autres possibilités) et que son nom est turntime. Quand il ya plusieurs arguments, vous devez les séparer par des virgules. Notez qu'en NXC, sub est comme void; En outre, les fonctions peuvent avoir d'autres types de retour que void, elles peuvent également renvoyer des valeurs entières ou une chaîne de caractère: pour plus de détails, consultez le guide NXC.

Définition de macros

Il ya encore une autre façon de donner un nom à de petits morceaux de code. Vous pouvez définir des macros en NXC (à ne pas confondre avec les macros dans BricxCC). Nous l'avons vu précédemment, il est possible de définir des constantes, en utilisant #define et en leur donnant un nom. Mais en réalité, on peut définir de cette manière n'importe quel morceau de code. Voici le même programme mais en utilisant une macro pour tourner.

#define turn_around \
    OnRev(OUT_B, 75); Wait(3400);OnFwd(OUT_AB, 75);
task main()
{
    OnFwd(OUT_AB, 75);
    Wait(1000);
    turn_around;
    Wait(2000);
    turn_around;
    Wait(1000);
    turn_around;
    Off(OUT_AB);
}

Après l'instruction #define le mot turn_around désigne le texte qui le suit. Maintenant, partout où vous mettrez turn_around, il sera remplacé par le texte de la définition. Notez que le texte doit être sur une seule ligne. (En fait, il existe des moyens de mettre un déclaration #define sur plusieurs lignes, mais ce n'est pas recommandé.)

Les déclarations #define sont en réalité beaucoup plus puissant. Ils peuvent aussi avoir des arguments. Par exemple, nous pouvons mettre le temps à tourner comme argument de la déclaration. Voici un exemple dans lequel nous définissons quatre macros : pour aller de l'avant, se déplacer vers l'arrière, tourner à gauche et à droite. Chacun possède deux arguments : la vitesse et la durée.

#define turn_right(s,t) \
    OnFwd(OUT_A, s);OnRev(OUT_B, s);Wait(t);
#define turn_left(s,t)   \
    OnRev(OUT_A, s);OnFwd(OUT_B, s);Wait(t);
#define forwards(s,t)    OnFwd(OUT_AB, s);Wait(t);
#define backwards(s,t)   OnRev(OUT_AB, s);Wait(t);
task main()
{
    backwards(50,10000);
    forwards(50,10000);
    turn_left(75,750);
    forwards(75,1000);
    backwards(75,2000);
    forwards(75,1000);
    turn_right(75,750);
    forwards(30,2000);
    Off(OUT_AB);
}

C'est vraiment très utile de définir de telles macros. Cela rend votre code plus compacte et plus lisible. Vous pouvez aussi plus facilement modifier votre code, pour changer la connexion d'un moteur par exemple.

Conclusion

Dans ce chapitre, vous avez vu l'utilisation des tâches, des sous-routines, des fonctions inline et des macros. Elles ont différentes utilités. Les tâches fonctionnent normalement au même moment et s'occupe des choses qui doivent se faire en même temps. Les sous-routines sont utiles pour les grands morceaux de code qui sont utilisés à différents endroits d'une même tâche. Les fonctions inline sont utiles pour les morceaux de code qui sont utilisés à différents endroits d'une même tâche mais cela utilise plus de mémoire. Finalement, les macros sont très utiles pour les petits morceaux de code qui doivent être utilisés à différents endroits. Ces macros peuvent avoir des paramètres de qui les rend très utiles.

Faire de la musique

Le NXT possède un haut-parleur intégré qui peut jouer des sonneries et même des fichiers audio. Ceci est particulièrement utile lorsque vous voulez que le NXT vous dise lorsque quelque chose se passe. Mais il peut aussi être drôle d'avoir un robot qui fasse de la musique ou qui parle pendant qu'il se promène.

À partir d'un fichier sonore

BricxCC possède un utilitaire de conversion de fichier .wav en fichier .rso. Cet utilitaire est accessible via le menu Tools > Sound conversion. Maintenant vous pouvez stocker un fichier .rso dans la mémoire flash du NXT en utilisant un autre accessoire, le navigateur de mémoire du NXT ( Tools > NXT explorer ) et l’exécuter via la commande :

PlayFileEx ( nom_fichier , volume , loop );

Ces arguments sont le nom du fichier son, le volume ( un nombre allant de 0 à 4 ) et loop : ce dernier est à 1 (TRUE) si vous voulez que le morceau soit joué en boucle ou 0 (FALSE) si vous voulez qu'il soit joué qu'une seule fois.

#define TIME 200
#define MAXVOL 7
#define MINVOL 1
#define MIDVOL 3
#define pause_4th Wait(TIME)
#define pause_8th Wait(TIME/2)
#define note_4th \
    PlayFileEx("! Click.rso",MIDVOL,FALSE); pause_4th
#define note_8th \
    PlayFileEx("! Click.rso",MAXVOL,FALSE); pause_8th
task main()
{
    PlayFileEx("! Startup.rso",MINVOL,FALSE);
    Wait(2000);
    note_4th;
    note_8th;
    note_8th;
    note_4th;
    note_4th;
    pause_4th;
    note_4th;
    note_4th;
    Wait(100);
}

Ce beau programme joue d'abord la mélodie de démarrage, que vous connaissez peut-être déjà. Les macros sont vraiment utiles dans ce cas pour simplifier la notation dans la tâche principale: essayez de modifier les réglages de volume ou d'ajouter des accents à la mélodie.

Jouer de la musique

Pour jouer une note, vous pouvez utiliser la commande :

PlayToneEx(frequency, duration, volume, loop?)

Qui possède 4 arguments. Le premier est la fréquence en Hertz, le second est la durée en millisecondes, le troisième est le volume et le dernier pour savoir si la musique tourne en boucle. Voici un tableau des fréquences utiles :


SON 3 4 5 6 7 8 9
SI 247 494 988 1976 3951 7902
LA# 233 466 932 1865 3729 7458
LA 220 440 880 1760 3520 7040 14080
SOL# 415 831 1661 3322 6644 13288
SOL 392 784 1568 3136 6272 12544
FA# 370 740 1480 2960 5920 11840
FA 349 698 1397 2794 5588 11176
MI 330 659 1319 2637 5274 10548
RE# 311 622 1245 2489 4978 9956
RE 294 587 1175 2349 4699 9398
DO# 277 554 1109 2217 4435 8870
DO 262 523 1047 2093 4186 8372

Comme avec la fonction PlayFileEx, le NXT n'attend pas la fin de la note pour passer à autre chose. Donc si vous utilisez plusieurs notes à la suites, vous devrez mettre de l'attente entre les deux comme dans l'exemple suivant :

#define VOL 3
task main()
{
  PlayToneEx(262,400,VOL,FALSE);  Wait(500);
  PlayToneEx(294,400,VOL,FALSE);  Wait(500);
  PlayToneEx(330,400,VOL,FALSE);  Wait(500);
  PlayToneEx(294,400,VOL,FALSE);  Wait(500);
  PlayToneEx(262,1600,VOL,FALSE); Wait(2000);
}

Vous pouvez créer très facilements des morceaux de musique avec les fonctionnalités de BricxCC.

Si vous voulez jouer de la musique tout faisant avancer le robot, la meilleure façon est d'utiliser des tâches distinctes. Ici vous avez un exemple d'un programme plutôt stupide où le robot avance en avant et en arrière, en faisant de la musique.

task music()
{
    while (true)
    {
        PlayTone(262,400);  Wait(500);
        PlayTone(294,400);  Wait(500);
        PlayTone(330,400);  Wait(500);
        PlayTone(294,400);  Wait(500);
    }
}
task movement()
{
    while(true)
    {
        OnFwd(OUT_AC, 75); Wait(3000);
        OnRev(OUT_AC, 75); Wait(3000);
    }
}
task main()
{
    Precedes(music, movement);
}

Conclusion

Dans ce chapitre, vous avez apprit à faire jouer de la musique au NXT. Vous avez aussi vu comment utiliser une tâche distincte pour jouer de la musique.

Plus sur les moteurs

Il y a beaucoup de fonctions qui vous permettent de contrôler les moteurs plus précisément. Dans ce chapitre, nous allons traiter de ces commandes : ResetTachoCount , Coast( Float ), OnFwdReg, OnRevReg, OnFwdSync, OnRevSync, RotateMotor, RotateMotorEx et les concepts de PID.

Arrêt en douceur

Quand on utilise la fonction Off (), le cerveau moteur s'arrête immédiatement, freinant l'arbre et tenant donc position. Il est aussi possible de stopper les moteurs d'une manière plus douce, sans utiliser le frein. Pour cela il faut utiliser Float() ou Coast(), qui coupe simplement la puissance du moteur. Voici un exemple. Le moteur commence par freiner en utilisant les freins; ensuite sans utiliser les freins. Notez la différence. Actuellement, la différence est extrêmement faible pour ce robot en particulier. Mais cela crée beaucoup de différence pour beaucoup d'autres robots.

task main()
{
    OnFwd(OUT_AC, 75);
    Wait(500);
    Off(OUT_AC);
    Wait(1000);
    OnFwd(OUT_AC, 75);
    Wait(500);
    Float(OUT_AC);
}

Les commandes avancées

Les commandes OnFwd() et OnRev() sont de simples routines pour faire bouger les moteurs.

Les servomoteurs du NXT contiennent un encodeur intégré qui vous permet de contrôler précisément la position et la vitesse de l'arbre;

Le firmware du NXT met en oeuvre un PID ( Proportional Integrative Derivative) pour contrôler la position des moteurs et la vitesse des moteurs. Cela permet de contrôler plus précisément la vitesse des moteurs, leur position et de faire des feedback.

Si vous voulez que le robot aille parfaitement droit, vous pouvez utiliser une fonctione de synchronisation qui fait que le couple de moteurs tournent ensemble et s'attendent si l'un ralentit ou est bloqué. D'une façon simple, vous pouvez définir un couple de moteur pour qu'il soient synchronisés. Il y a aussi de nombreuses commandes pour libérer toute la puissance des cerveau-moteurs.

La fonction OnFwdReg(port,speed,regmode) fait avancer le moteur spécifié par port à la vitesse speed en appliquant le mode de régulation regmode spécifié. regmode peut être OUT_REGMODE_IDLE, OUT_REGMODE_SPEED ou OUT_REGMODE_SYNC. Si IDLE est sélectionné, il n'y aura pas de régulation PID d'appliqué. Si SPEED est sélectionné, le NXT va réguler LE moteur pour qu'il ai une vitesse constante, même si la charge moteur varie. Pour finir, si SYNC est sélectionné, le couple de moteurs spécifié par port se déplacera synchronisé comme expliqué avant.

La fonction OnRevReg(port,speed,regmode) fait exactement la même chose que la commande précédente mais dans le sens inverse.

task main()
{
    OnFwdReg(OUT_AC,50,OUT_REGMODE_IDLE);
    Wait(2000);
    Off(OUT_AC);
    PlayTone(4000,50);
    Wait(1000);
    ResetTachoCount(OUT_AC);
    OnFwdReg(OUT_AC,50,OUT_REGMODE_SPEED);
    Wait(2000);
    Off(OUT_AC);
    PlayTone(4000,50);
    Wait(1000);
    OnFwdReg(OUT_AC,50,OUT_REGMODE_SYNC);
    Wait(2000);
    Off(OUT_AC);
}

Ce programme montre très bien les différentes régulation si vous essayez de stopper les roues avec votre main. Pour le premier ( le mode IDLE ), si vous stopper une roue, cela de fera rien sur l'autre. Pour le second ( le mode SPEED ), essayer de ralentir la roue, vous verrez que le NXT augmentera la puissance de son moteur pour contrecarrer votre freinage afin de garder la vitesse constante. Enfin (pour le mode SYNC), arrêter une roue fera arrêter l'autre jusqu'à ce que la première soit débloquée.

La fonction OnFwdSync(port,speed,turnpct) est la même que la fonction OnFwdReg() avec le mode SYNC, mais maintenant vous pouvez spécifier un pourcentage de synchronisation.

La fonction OnRevSync(port,speed,turnpct) est la même que la précédente, mais dans l'autre direction. Le programme suivant fait une démonstration des fonctions : essayez de changer les valeurs pour voir ce qu'il se passe.

task main()
{
  PlayTone(5000,30);
  OnFwdSync(OUT_AC,50,0);
  Wait(1000);
  PlayTone(5000,30);
  OnFwdSync(OUT_AC,50,20);
  Wait(1000);
  PlayTone(5000,30);
  OnFwdSync(OUT_AC,50,-40);
  Wait(1000);
  PlayTone(5000,30);
  OnRevSync(OUT_AC,50,90);
  Wait(1000);
  Off(OUT_AC);
}

Pour finir, les moteurs peuvent être configurés pour tourner un nombre défini de degrés ( en se rappelant qu'un tour complet fait 360° ).

Pour chacune des fonctions suivantes, vous pouvez agir sur la direction des moteurs en changeant le signe de la vitesse ou de l'angle. Donc si la vitesse et l'angle on le même signe, le moteur avancera, sinon il reculera.

La fonction RotateMotor(port,speed,degrees) fait tourner le moteur spécifié par port de degrees degrés à une vitesse de speed ( allant de 0 à 100 ).

task main()
{
    RotateMotor(OUT_AC, 50,360);
    RotateMotor(OUT_C, 50,-360);
}

La fonction RotateMotorEx(port,speed,degrees,turnpct,sync,stop) est une extension de la fonction précédente, ce qui vous permet de synchroniser deux moteurs, de spécifier un facteur turnpct et un booleen pour sync. Cela vous permet aussi de spécifier si le moteur doit freiner apres la rotation de l'angle spécifié en utilisant un dernier booleen.

task main()
{
  RotateMotorEx(OUT_AC,50,360,0,true,true);
  RotateMotorEx(OUT_AC,50,360,40,true,true);
  RotateMotorEx(OUT_AC,50,360,-40,true,true);
  RotateMotorEx(OUT_AC,50,360,100,true,true);
}

Les PID de contrôle

Le firmware du NXT intègre un contrôleur digital de PID ( Proportional Integrative Derivative ) pour réguler la position et la vitesse des cervau-moteurs avec précision. Ce type de contrôleur est l'un des plus simples et des plus efficaces dans l'automatisme, et est souvent utilisé.

En termes bruts, il fonctionne comme cela :

Votre programme donne au contrôleur un point fixe R(t) à atteindre, celui-ci démarre le moteur avec une commande U(t), mesure sa position Y(t) grâce au calculateur intégré et calcule une erreur E(t) = R(t) - Y(t): voici pourquoi il est appelé "contrôleur en boucle fermée", parce que la position de sortie Y(t) est ramené à l'entrée du contrôleur pour le calcul de l'erreur. Le contrôleur transforme l'erreur E(t) dans la commande U(t) ainsi:

 U(t) = P(t) + I(t) + D(t)

Avec :

 P(t) = K_P*E(t)
 I(t) = K_I*( I(t-1) + E(t) )
 D(t) = K_D*( E(t) - E(t-1) )
 

Cela peut sembler compliqué pour un novice, mais je vais essayer d'expliquer le mécanisme du mieux que je peux.

La fonction est la somme de trois parties : la partie proportionnelle P(t), la partie intégrative I(t) et la partie dérivative D(t).

P(t) rend le contrôleur plus rapide dans le temps mais il n'assure pas qu'il n'y ai aucune erreur d'équilibrage.

I(t) donne de la "mémoire" au contrôleur, dans un sens il accumules les traces d'erreurs et les compense, avec un zéro erreur de moyenne garantie.

D(t) donnes les "prédiction futures" au contrôleur ( comme la dérivée en maths ), ce qui accélère la réponse.

Je comprend que ça peut rester confus. Mais on peut maintenant le tester ensemble pour mieux comprendre. Le programme simple qui suit va nous y aider.

#define P 50
#define I 50
#define D 50
task main(){
   RotateMotorPID(OUT_A, 100, 180, P, I, D);
   Wait(3000);
}

La fonction RotateMotorPID ( port , speed , Pgain , Igain , Dgain ) vous permet de régler les valeurs des différents gains autrement que celles par défaut. Essayer de définir les valeur suivantes :

- ( 50 , 0 , 0 ) : le moteur ne tourne pas exactement à 180°, car il reste une erreur non compensée
- ( 0 , x , x ) : sans partie proportionnelle, l'erreur est énorme.
- ( 40 , 40 , 0 ) : il ya un dépassement, cela signifie que les mouvements de l'arbre du moteur vont au-delà du point voulu, il va donc faire demi-tour.
- ( 40 , 40 , 90 ) : bonne précision et rapide ( pour le temps à atteindre le point voulu )
- ( 40 , 40 , 200 ) : l'arbre moteur oscille car le gain de dérivée est trop élevé.

Essayez d'autres valeur pour comprendre comment ces gains influences les performances du moteur.

Conclusion

Dans ce chapitre avez appris des choses à propos des commandes avancées des moteurs : Float() , Coast() qui stoppent les moteurs doucement; OnXxxReg() et OnXxxSync() qui permettent de contrôler le retour des moteurs, la vitesse et la synchronisation; RotateMotor() et [[[RotateMotorEx]]]() qui sont utilisés pour tourné l'arbre du moteur d'un nombre précis de degrés. Vous avez aussi appris quelque chose à propos du contrôle PID. il n'a peut-être pas eu d'explications exhaustives, mais peut-être cela à créé un peu de curiosité en vous: recherchez sur le web !

Plus sur les capteurs

Nous avons déjà utilisé les capteurs dans un chapitre précédent. Mais vous pouvez faire beaucoup plus de choses avec les capteurs. Dans ce chapitre, nous allons voir les différences entre les différents modes et les différents types des capteurs, on va aussi voir comment utiliser les vieux capteurs compatible RCX en le connectant au NXT via un câble convertisseur.

Modes et types des capteurs

La fonction SetSensor() que nous avons vu avant fait actuellement deux choses : il défini le type de capteurs et défini le mode dans lequel le capteur opère. En configurant le mode et le type du capteur séparément, on peut contrôler le comportement du capteur avec plus de précision, ce qui est très pratique pour certaines applications.

Le type du capteur est défini grâce à la fonction SetSensorType(). Il y a beaucoup de types différents, mais on va voir les principaux : SENSOR_TYPE_TOUCH, le capteur tactile, SENSOR_TYPE_LIGHT_ACTIVE, le capteur de couleur ( avec les DELs allumées ), SENSOR_TYPE_SOUND_DB, le capteur sonore et SENSOR_TYPE_LOWSPEED_9V, le capteur à ultrasons. Définir le type de capteur est particulièrement important pour indiquer si le capteur à besoin d'énergie ( pour allumer les DELs du capteur de couleur par exemple ), ou pour indiquer au NXT que le capteur est digital et à besoin d'être lu via le protocole IC en série. C'est possible d'utiliser les vieux capteur RCX avec le NXT : SENSOR_TYPE_TEMPERATURE, pour le capteur de température, SENSOR_TYPE_LIGHT pour le vieux capteur de couleur, SENSOR_TYPE_ROTATION pour le capteur RCX de rotation ( on en parlera plus tard ).

Le mode du capteur est défini grâce à la fonction SetSensorMode(). Il y a huit différents modes. Le plus important est SENSOR_MODE_RAW. Dans ce mode, la valeur que vous recevez du capteur est un nombre entre 0 et 1023. C'est la valeur brut produite par le capteur. La signification dépend du capteur. Par exemple, pour un capteur tactile, quand le capteur n'est pas enfoncé, la valeur est proche de 1023. Quand il est complètement enfoncé, la valeur est proche de 50. Quand il est partiellement enfoncé, la valeur est entre 50 et 1000. Donc si vous définissez le mode RAW, vous pouvez savoir s'il est partiellement enfoncé. Avec le capteur de couleur, la valeur varie entre 300 ( beaucoup de lumière ) et 800 ( très sombre ). Cela donne une valeur plus précise que d'utiliser la fonction SetSensor(). Pour plus de détail, regardez le guide de programmation NXC.

Le deuxième mode est SENSOR_MODE_BOOL. Avec ce mode, la valeur retournée est 0 ou 1. Quand la valeur du mode RAW est en dessous de 562 la valeur est 0, sinon la valeur est 1. SENSOR_MODE_BOOL est la valeur par défaut du capteur tactile, mais il peut être utilisé pour d'autres capteurs qui renvoient des valeurs analogiques. Les modes SENSOR_MODE_CELCIUS et SENSOR_MODE_FAHRENHEIT ne sont utiles qu'avec les capteurs de températures. SENSOR_MODE_PERCENT convertit la valeur brut en une valeur comprise entre 0 et 100. SENSOR_MODE_PERCENT est le mode par défaut du capteur de couleur. SENSOR_MODE_ROTATION n'est utilisé qu'avec le capteur de rotation.

Il y a deux autres modes intéressants : SENSOR_MODE_EDGE et SENSOR_MODE_PULSE. Ils comptent le nombre de transitions entre une valeur faible et une valeur élevée ou opposée. Par exemple, quand on touche un capteur tactile, cela cause une transition d'une valeur haute à une valeur basse. Quand on le relâche, cela crée une autre transition. Quand vous définissez le mode à SENSOR_MODE_PULSE, seules les transition de faibles à élevées sont comptées. Donc chaque pression et relâchement comptent pour une seule fois. Quand vous définissez le mode à SENSOR_MODE_EDGE, chaque transitions sont comptées. Donc chaque pression et relâchement comptent pour deux fois. Quand vous comptez les pressions ou impulsions, vous devez reinitialiser les compteurs à 0. Vous pouvez le faire grâce à la fonction ClearSensor(), vous réinitialisez le compteur indiqué à 0.

Regardons un exemple. Le programme suivant utilise le capteur tactile pour diriger le robot. Connectez le capteur tactile avec un long câble à l'entrée une. Si vous pressez deux fois, le robot recule. Si vous le pressez une fois, il s'arrête de bouger.

task main()
{
	SetSensorType(IN_1, SENSOR_TYPE_TOUCH);
	SetSensorMode(IN_1, SENSOR_MODE_PULSE);
	while(true)
	{
		ClearSensor(IN_1);
		until (SENSOR_1 > 0);
		Wait(500);
		if (SENSOR_1 == 1) {Off(OUT_AC);}
		if (SENSOR_1 == 2) {OnFwd(OUT_AC, 75);}
	}
}

Notez que nous commençons par définir le type et ensuite le mode. C'est essentiel car changer le type affecte le mode.

Capteur de rotation

Le capteur de rotation est un type de capteur très utile. Le capteur de rotation possède un trou dans lequel vous pouvez passer un axe. Ce qui permettra de mesurer la position angulaire relative. Une rotation complète de l'axe compte 16 étapes ( ou -16 si la rotation se fait dans l'autre sens ), ce qui fait 22,5° l'étape... Loin du respect à 1° près des servomoteurs. L'avantage du vieux capteur est qu'il est très facile à faire tourner alors que les nouveaux nécessitent un couple important.

Si vous avez besoin d'une résolution plus fine que 16 pas par tour, vous pouvez toujours utiliser une démultiplication pour augmenter le nombre de pas par tour.

Une application standard est d'avoir deux capteurs de rotations relié aux deux roues du robot que vous contrôlez avec les deux moteurs. Pour un mouvement rectiligne vous voulez que les deux roues tournent à la même vitesse. Malheureusement, les moteurs ne fonctionnent pas toujours à la même vitesse. En utilisant les capteurs de rotation, vous pouvez voir qu'une roue tourne plus vite. Vous pouvez alors arrêter temporairement un moteur ( en utilisant float ()) jusqu'à ce que les capteurs soient de nouveau à la même vitesse. Le programme qui suit fait cela. Il permet tout simplement au robot d'avancer en ligne droite. Pour l'utiliser, modifier votre robot en reliant les deux capteurs de rotation pour les deux roues. Branchez les capteurs à l'entrée 1 et 3.

task main()
{
	SetSensor(IN_1, SENSOR_ROTATION);
	ClearSensor(IN_1);
	SetSensor(IN_3, SENSOR_ROTATION);
	ClearSensor(IN_3);
	while (true)
	{
		if (SENSOR_1 < SENSOR_3) {
			OnFwd(OUT_A, 75);
			Float(OUT_C);
		}
		else if (SENSOR_1 > SENSOR_3) {
			OnFwd(OUT_C, 75);
			Float(OUT_A);
		}
		else
			OnFwd(OUT_AC, 75);
	}
}

Le programme indique d'abord que les deux capteurs sont des capteurs de rotation, et les remet à zéro. Ensuite, il commence une boucle infinie. Dans la boucle, nous vérifions que les deux valeurs du capteur sont égales. Si elles le sont, le robot se déplace tout droit. Si l'une est plus grande, le moteur s'arrête jusqu'à ce que les deux valeurs soient à nouveau égales.

Clairement, ce programme est très simple. Vous pouvez étendre ceci pour que le robot fasse une distance très précise.

Mettre plusieurs capteurs sur une entrée

En raison de la nouvelle structure des capteurs NXT et des câbles 6-fils, il n'est pas facile de faire comme avant (comme cela a été fait pour le RCX) pour connecter plusieurs capteurs sur le même port. À mon humble avis, la seule solution fiable serait de construire un capteur tactile multiplexeur analogique à utiliser en combinaison avec un câble convertisseur. L'alternative est un multiplexeur numérique complexe qui permet de gérer la communication I2C avec NXT, mais ce n'est pas vraiment une solution abordable pour les débutants.

Le NXT possède quatre entrées pour connecter des capteurs. Si vous voulez faire des robots plus complexes (et vous avez acheté des capteurs supplémentaires) cela pourrait ne pas être suffisant pour vous. Heureusement, avec quelques astuces, vous pouvez connecter deux (voir plus) capteurs sur une entrée.

Le plus simple est de brancher deux capteurs de contact à une entrée. Si l'un d'eux (ou les deux) est touché la valeur est 1, sinon il est égale à 0. Vous ne pouvez pas distinguer les deux, mais parfois ce n'est pas nécessaire. Par exemple, lorsque vous mettez un capteur tactile à l'avant et un à l'arrière du robot, vous savez que l'on est touché et vous modifiez l'orientation du robot. Mais vous pouvez aussi changer le mode de l'entrée à RAW ce qui vous permet d'obtenir beaucoup plus d'informations. Si vous êtes chanceux, la valeur lorsqu'un capteur sera pressé ne sera pas la même pour les deux capteurs. Si tel est le cas, vous pouvez effectivement faire la distinction entre les deux capteurs. Et quand les deux sont pressés vous obtenez une valeur beaucoup plus faible (environ 30) de sorte que vous pouvez également le détecter.

Vous pouvez également connecter un capteur tactile et un capteur de lumière à une entrée (capteurs RCX seulement). Définissez le type de ce port comment capteur de lumière (sinon le capteur de lumière ne fonctionne pas). Sélectionnez le mode RAW. Dans ce cas, lorsque le capteur de contact est poussée vous obtenez une valeur RAW inférieure à 100. Si ce n'est pas poussé, vous obtenez la valeur du capteur de lumière, qui n'est jamais en dessous de 100. Le programme suivant utilise cette idée. Le robot doit être équipé d'un capteur de lumière orienté vers le bas, et un pare-chocs à l'avant relié à un capteur tactile. Connectez les deux capteurs à l'entrée 1. Le robot avancera au hasard dans une zone. Lorsque le capteur de lumière voit une ligne sombre ( valeur RAW > 750) il recule un peu. Lorsque le capteur tactile touche quelque chose (valeur RAW inférieure à 100), il fait la même chose. Voici le programme:

mutex moveMutex;
int ttt,tt2;
 
task moverandom()
{
	while (true)
	{
		ttt = Random(500) + 40;
		tt2 = Random();
		Acquire(moveMutex);
		if (tt2 > 0) {
			OnRev(OUT_A, 75);
			OnFwd(OUT_C, 75);
			Wait(ttt);
		}
		else {
			OnRev(OUT_C, 75);
			OnFwd(OUT_A, 75);
			Wait(ttt);
		}
		ttt = Random(1500) + 50;
		OnFwd(OUT_AC, 75);
		Wait(ttt);
		Release(moveMutex);
	}
}
 
task submain()
{
	SetSensorType(IN_1, SENSOR_TYPE_LIGHT);
	SetSensorMode(IN_1, SENSOR_MODE_RAW);
	while (true)
	{
		if ((SENSOR_1 < 100) || (SENSOR_1 > 750))
		{
			Acquire(moveMutex);
			OnRev(OUT_AC, 75); Wait(300);
			Release(moveMutex);
		}
	}
}
 
task main()
{
	Precedes(moverandom, submain);
}

J'espère que le programme est clair. Il ya deux tâches. La tâche moverandom fait bouger le robot d'une manière aléatoire. La tâche principale démarre en premier moverandom, définit le capteur, puis attend que quelque chose se passe. Si la valeur capteur est trop basse (touché) ou trop élevé (sur la zone blanche), il arrête les mouvements aléatoires, recule un peu, et recommence les mouvements aléatoires.

Conclusion

Dans ce chapitre, nous avons vu un certain nombre de solutions supplémentaires sur les capteurs. Nous avons vu comment régler séparément le type et le mode d'un capteur et comment cela pourrait être utilisé pour obtenir des informations supplémentaires. Nous avons appris à utiliser les capteurs de rotation. Et nous avons vu comment de multiples capteurs peuvent être connectés à une unique entrée du NXT. Tous ces trucs sont très utiles lors de la construction de robot plus complexes dans lesquels les capteurs jouent toujours un rôle primordial.

Les taches parallèles

Comme cela a été indiqué précédemment, les tâches sont exécutées simultanément NXC, ou en parallèle. Cela est extrêmement utile. Cela vous permet d'observer les capteurs dans une tâche tout en exécutant une autre tâche qui s'occupe du déplacement du robot. Mais les tâches parallèles peuvent aussi causer des problèmes. Une tâche peut interférer avec une autre.

Un mauvais programme

Considérez le programme suivant. Voici une tâche faisant décrire un carré au robot (comme nous l'avons fait si souvent avant) et la deuxième tâche s'occupe des capteurs tactiles. Lorsqu'un capteur est touché, le robot se déplace un peu vers l'arrière, et fait un virage à 90 degrés.

task check_sensors()
{
	while (true)
	{
		if (SENSOR_1 == 1)
		{
			OnRev(OUT_AC, 75);
			Wait(500);
			OnFwd(OUT_A, 75);
			Wait(850);
			OnFwd(OUT_C, 75);
		}
	}
}
 
task submain()
{
	while (true)
	{
		OnFwd(OUT_AC, 75); Wait(1000);
		OnRev(OUT_C, 75); Wait(500);
	}
}
 
task main()
{
	SetSensor(IN_1,SENSOR_TOUCH);
	Precedes(check_sensors, submain);
}

Ce programme vous semble probablement tout à fait valable. Mais si vous l'exécutez, vous verrez un comportement imprévu. Essayez ce qui suit: faite touche quelque chose au robot pendant qu'il tourne. Il va commencer revenir en arrière, mais se déplace immédiatement vers l'avant à nouveau, atteignant l'obstacle. La raison vient du fait que les tâches peuvent interférer entre elles. Voici ce qu'il se passe. Le robot tourne à droite, voulu par la première tâche, la deuxième tâche est en sommeil. Maintenant, le robot rencontre un obstacle. Il va commencer à reculer, mais à ce moment-là, la tâche principale se réveille et déplace le robot vers l'avant à nouveau : dans l'obstacle. La deuxième tâche esgt en sommeil à ce moment et ne detecte pas la collision. Ce n'est clairement pas le comportement que nous aimerions voir. Le problème est que, tandis que la seconde tâche est en sommeil nous n'avions pas réalisé que la première tâche était encore en marche, et que ses actions interféraient avec les actions de la deuxième tâche.

Sections critiques et les mutex

Voici une façon de résoudre ce problème et de s'assurer qu'à tout moment une seule tâche manipule le robot. Ce fut l'approche, nous avons pris dans un chapitre précédent.

mutex moveMutex;
 
task move_square()
{
	while (true)
	{
		Acquire(moveMutex);
		OnFwd(OUT_AC, 75);
		Wait(1000);
		OnRev(OUT_C, 75);
		Wait(850);
		Release(moveMutex);
	}
}
 
task check_sensors()
{
	while (true)
	{
		if (SENSOR_1 == 1)
		{
			Acquire(moveMutex);
			OnRev(OUT_AC, 75);
			Wait(500);
			OnFwd(OUT_A, 75);
			Wait(850);
			Release(moveMutex);
		}
	}
}
 
task main()
{
	SetSensor(IN_1,SENSOR_TOUCH);
	Precedes(check_sensors, move_square);
}

Le point crucial est que chacune des tâches check_sensors et move_square peuvent commander les moteurs que si aucune autre tâche n'est en train de les utiliser : cela se fait en utilisant l'instruction Acquire qui attend que la variable d'exclusion mutuelle moveMutex soit libérée avant d'utiliser les moteurs. La réciproque de la commande Acquire est la commande Release, qui libère la variable mutex pour que d'autres tâches puissent utiliser la ressource critique, les moteurs dans notre cas. Le code à l'intérieur des Acquire-Release est appelée région critique : critiques signifie que ces ressources partagées sont utilisées. De cette façon, les tâches ne peuvent pas interférer les unes avec les autres.

En utilisant des semaphores

Il existe une alternative aux variable mutex qui est la mise en œuvre explicite des commandes Acquire et Release.

Une technique standard pour résoudre ce problème consiste à utiliser une variable pour indiquer quelle tâche à le contrôle des moteurs. Les autres tâches ne sont pas autorisées à contrôler les moteurs jusqu'à ce que la première tâche l'indique, en utilisant une variable. Une telle variable est souvent appelé un sémaphore. Prennont sem comme étant un sémaphore ( comme un mutex ). Nous supposons que la valeur 0 indique qu'aucune tâche ne contrôle les moteurs (les ressources sont libres). Maintenant, chaque fois qu'une tâche veut faire quelque chose avec les moteurs il exécute les commandes suivantes:

until (sem == 0);
sem = 1; //Acquire(sem);
// Fait quelque chose avec les moteurs
// C'est la région critique
sem = 0;  //Release(sem);

Donc, nous attendons d'abord que personne n'utilise les moteurs. Ensuite, nous demandons le contrôle par la mise à 1 de sem. Maintenant, nous pouvons contrôler les moteurs. Quand nous auront fini, nous mettrons sem à 0. Regardez le programme ci-dessus, qui utilise les sémaphores. Lorsque le capteur tactile touche quelque chose, le sémaphore est à 1 et la procédure de recul est en marche. Au cours de cette procédure, la tâche move_square doit attendre. Au moment où le recul est terminé, le sémaphore est remis à 0 et la tâche move_square peut continuer.

int sem;
task move_square()
{
	while (true)
	{
		until (sem == 0);
		sem = 1;
		OnFwd(OUT_AC, 75);
		sem = 0;
		Wait(1000);
		until (sem == 0);
		sem = 1;
		OnRev(OUT_C, 75);
		sem = 0;
		Wait(850);
	}
}
 
task submain()
{
	SetSensor(IN_1, SENSOR_TOUCH);
	while (true)
	{
		if (SENSOR_1 == 1)
		{
			until (sem == 0);
			sem = 1;
			OnRev(OUT_AC, 75);
			Wait(500);
			OnFwd(OUT_A, 75);
			Wait(850);
			sem = 0;
		}
	}
}
 
task main()
{
	sem = 0;
	Precedes(move_square, submain);
}

On pourrait dire qu'il n'est pas nécessaire dans move_square de mettre le sémaphore à 1 et ensuite à 0. Pourtant c'est utile. La raison en est que le OnFwd() commande est en fait deux commandes (voir chapitre précédent). Vous ne voulez pas que cette séquence de commande soit interrompue par l'autre tâche.

Les sémaphores sont très utiles et, lorsque vous écrivez des programmes compliqués avec des tâches parallèles, ils sont presque toujours nécessaire. (Il ya encore une petite risque qu'ils échouent. Essayez de comprendre pourquoi.)

Conclusion

Dans ce chapitre, nous avons étudié quelques-uns des problèmes qui peuvent survenir lorsque vous utilisez différentes tâches. Toujours faire très attention aux effets secondaires. Une grande partie du comportement inattendu est dû à cela. Nous avons vu deux différentes façons de résoudre ces problèmes. La première solution est d'arrêter et de redémarrer une tâche afin de s'assurer qu'une seule tâche critique est en cours d'exécution à chaque instant. La deuxième méthode utilise les sémaphores pour contrôler l'exécution des tâches. Cela garantit que, à chaque instant, la partie critique d'une tâche est exécutée.

Communications entre robots

Si vous possédez plus d'un NXT ce chapitre est fait pour vous (mais vous pouvez toujours communiquer des données au PC, si vous n'avez qu'un seul NXT). Les robots peuvent communiquer entre eux via la technologie radio Bluetooth: vous pouvez faire collaborer plusieurs robots (ou les faire se battre), et vous pouvez construire un grand robot complexe au moyen de deux NXTs, de sorte que vous pouvez utiliser six moteurs et huit capteurs.

Pour le bon vieux RCX, c'est simple : il envoie un message infrarouge et tous les robots autour le reçoivent.

Pour le NXT c'est une autre histoire! Tout d'abord, vous devez connecter deux ou plusieurs NXTs (ou NXT PC) via le menu Bluetooth, et alors seulement vous pourez envoyer des messages aux dispositifs connectés.

Le NXT qui démarre la connexion est appelé maître, et peut avoir jusqu'à 3 périphériques esclaves connectés sur les lignes 1,2,3; les esclaves ont le Maître connecté sur la ligne 0. Vous pouvez envoyer des messages à 10 boîtes aux lettres disponibles.

Messagerie maître/esclave

On va voir deux programme, un pour le maître, l'un pour l'esclave. Ces programmes de base vont vous apprendre comment un flux continu de messages rapides peut être géré par un réseau sans fil à deux NXT.

Le programme du maître vérifie d'abord si l'esclave est correctement connecté à la ligne 1 (la constante BT_CONN) à l'aide de la fonction BluetoothStatus(conn); puis construit et envoie des messages avec un préfixe M et un nombre croissant avec SendRemoteString(conn, queue, string), tandis que l'on reçoit des messages de l'esclave avec ReceiveRemoteString(queue, clear, string) et affiche les données.

//MAÎTRE
 
#define BT_CONN 1
#define INBOX 1
#define OUTBOX 5
 
sub BTCheck(int conn){
	if (!BluetoothStatus(conn)==NO_ERR){
		TextOut(5,LCD_LINE2,"Error");
		Wait(1000);
		Stop(true);
	}
}
 
task main(){
	string in, out, iStr;
	int i = 0;
	BTCheck(BT_CONN); //check slave connection
	while(true){
		iStr = NumToStr(i);
		out = StrCat("M",iStr);
		TextOut(10,LCD_LINE1,"Master Test");
		TextOut(0,LCD_LINE2,"IN:");
		TextOut(0,LCD_LINE4,"OUT:");
		ReceiveRemoteString(INBOX, true, in);
		SendRemoteString(BT_CONN,OUTBOX,out);
		TextOut(10,LCD_LINE3,in);
		TextOut(10,LCD_LINE5,out);
		Wait(100);
		i++;
	}
}

Le programme esclave est très similaire, mais utilise SendResponseString(queue,string) au lieu de SendRemoteString parce l'esclave doit pouvoir envoyer des messages seulement à son maître, sur la ligne 0.

//ESCLAVE
 
#define BT_CONN 1
#define INBOX 5
#define OUTBOX 1
 
sub BTCheck(int conn){
	if (!BluetoothStatus(conn)==NO_ERR){
		TextOut(5,LCD_LINE2,"Error");
		Wait(1000);
		Stop(true);
	}
}
 
task main(){
	string in, out, iStr;
	int i = 0;
	BTCheck(0); //teste la connexion avec l'esclave
	while(true){
		iStr = NumToStr(i);
		out = StrCat("S",iStr);
		TextOut(10,LCD_LINE1,"Slave Test");
		TextOut(0,LCD_LINE2,"IN:");
		TextOut(0,LCD_LINE4,"OUT:");
		ReceiveRemoteString(INBOX, true, in);
		SendResponseString(OUTBOX,out);
		TextOut(10,LCD_LINE3,in);
		TextOut(10,LCD_LINE5,out);
		Wait(100);
		i++;
	}
}

Vous remarquerez que si un des programmes s'arrête, l'autre continuera à envoyer des messages avec un nombre croissant, sans savoir que tous les messages envoyés seront perdus, car personne ne l'écoute de l'autre côté. Pour éviter ce problème, nous pourrions prévoir un protocole plus fin, avec accusé de réception.

Envoi de numéros avec accusés

Ici, nous voyons un autre couple de programmes : cette fois, le maître envoie des nombres avec SendRemoteNumber(conn, queue, numbre) et cesse d'attendre les accusés de l'esclave; que si l'esclave est à l'écoute et envoi l'accusé, le maître continue en envoyant le message suivant. Les esclaves reçoivent simplement le numéro avec ReceiveRemoteNumber(queue, clear, number) et envoie l'accusé avec SendResponseNumber. Vos programmes maître-esclave doivent avoir un code commun pour les accusés de réception, personnelement, je choisis la valeur 0xFF hexadecimale.

Le maître envoie des nombres aléatoires et attend les accusés esclaves; chaque fois qu'il reçoit un accusé de réception avec le bon code, la variable ack doit être réinitialisée, faute de quoi le maître continuera à envoyer sans nouveaux accusés, car la variable est nulle.

L'esclave vérifie en permanence la boîte aux lettres et, si elle n'est pas vide, affiche la valeur lue et envoie un accusé de réception au maître. Au début du programme, j'ai choisi d'envoyer un accusé de réception sans la lecture des messages pour débloquer le maître, en fait, sans cette astuce, si le programme maître est lancé en premier, il serait mort, même si nous lançons un esclave plus tard. De cette façon, les premiers messages peuvent se perdre, mais vous pouvez démarrer des programmes maître et esclave à différents moments, sans risque d'accrochage.

//MAÎTRE
 
#define BT_CONN 1
#define OUTBOX 5
#define INBOX 1
#define CLEARLINE(L)   \
		TextOut(0,L,"                   ");
 
sub BTCheck(int conn){
	if (!BluetoothStatus(conn)==NO_ERR){
		TextOut(5,LCD_LINE2,"Error");
		Wait(1000);
		Stop(true);
	}
}
 
task main(){
	int ack;
	int i;
	BTCheck(BT_CONN);
	TextOut(10,LCD_LINE1,"Master sending");
	while(true){
		i = Random(512);
		CLEARLINE(LCD_LINE3);
		NumOut(5,LCD_LINE3,i);
		ack = 0;
		SendRemoteNumber(BT_CONN,OUTBOX,i);
		until(ack==0xFF) {
			until(ReceiveRemoteNumber(INBOX,true,ack) == NO_ERR);
		}
		Wait(250);
	}
}


//ESCLAVE
#define BT_CONN 1
#define OUT_MBOX 1
#define IN_MBOX 5
 
sub BTCheck(int conn){
	if (!BluetoothStatus(conn)==NO_ERR){
		TextOut(5,LCD_LINE2,"Error");
		Wait(1000);
		Stop(true);
	}
}
 
task main(){
	int in;
	BTCheck(0);
	TextOut(5,LCD_LINE1,"Slave receiving");
	SendResponseNumber(OUT_MBOX,0xFF); //unblock master
	while(true){
		if (ReceiveRemoteNumber(IN_MBOX,true,in) != STAT_MSG_EMPTY_MAILBOX) {
			TextOut(0,LCD_LINE3,"                   ");
			NumOut(5,LCD_LINE3,in);
			SendResponseNumber(OUT_MBOX,0xFF);
		}
		Wait(10); // pause (optionnelle)
	}
}

Envoi de commandes directes

Il ya une autre fonctionnalité intéressante sur la communication Bluetooth: le maître peut contrôler directement ses esclaves.

Dans l'exemple suivant, le maître envoie à l'esclave directement les commandes pour jouer des sons et déplacer un moteur, il n'y a pas besoin d'un programme pour l'esclave, puisque c'est le firmware du NXT esclave qui reçoit et gère les messages!

//MAÎTRE
 
#define BT_CONN 1
#define MOTOR(p,s) RemoteSetOutputState(BT_CONN, p, s, \
	OUT_MODE_MOTORON+OUT_MODE_BRAKE+OUT_MODE_REGULATED, \
	OUT_REGMODE_SPEED, 0, OUT_RUNSTATE_RUNNING, 0)
 
sub BTCheck(int conn){
	if (!BluetoothStatus(conn)==NO_ERR){
		TextOut(5,LCD_LINE2,"Error");
		Wait(1000);
		Stop(true);
	}
}
 
task main(){
	BTCheck(BT_CONN);
	RemotePlayTone(BT_CONN, 4000, 100);
	until(BluetoothStatus(BT_CONN)==NO_ERR);
	Wait(110);
	RemotePlaySoundFile(BT_CONN, "! Click.rso", false);
	until(BluetoothStatus(BT_CONN)==NO_ERR);
	//Wait(500);
	RemoteResetMotorPosition(BT_CONN,OUT_A,true);
	until(BluetoothStatus(BT_CONN)==NO_ERR);
	MOTOR(OUT_A,100);
	Wait(1000);
	MOTOR(OUT_A,0);
}

Conclusion

Dans ce chapitre, nous avons étudié quelques-uns des aspects fondamentaux de la communication Bluetooth entre robots: connexion de deux NXTs, envoyer et recevoir des chaînes de caractères, des nombres et des accusés de reception. Ce dernier aspect est très important quand un protocole de communication sécurisé est nécessaire.

Comme fonction supplémentaire, vous avez également appris à envoyer des commandes directement à une brique esclave.

Plus de commandes

Le NXC a un certain nombre de commandes supplémentaires. Dans ce chapitre, nous allons discuter de trois types : l'utilisation de la minuterie, les commandes pour contrôler l'affichage et l'utilisation du système de fichiers NXT.

Temporisateurs

Le NXT a une minuterie qui fonctionne en permanence. Cette minuterie tiques par incrémentations de 1 / 1000 de seconde. Vous pouvez obtenir la valeur actuelle de la minuterie avec CurrentTick(). Voici un exemple de l'utilisation d'une minuterie. Le programme suivant permet de bouger aléatoirement pendant 10 secondes.

task main()
{
	long t0, time;
	t0 = CurrentTick();
	do
	{
		time = CurrentTick()-t0;
		OnFwd(OUT_AC, 75);
		Wait(Random(1000));
		OnRev(OUT_C, 75);
		Wait(Random(1000));
	}
	while (time<10000);
	Off(OUT_AC);
}

Vous pouvez comparer ce programme avec celui donné dans le chapitre IV qui fait exactement la même tâche. Celle avec une minuterie est nettement plus simple.

Les Timers sont très utiles en remplacement du Wait(). Vous pouvez endormir votre programme pour un temps donné en attendant qu'elle atteigne une valeur particulière. Mais vous pouvez aussi réagir sur d'autres événements (par exemple des capteurs) en attendant. Le programme suivant en est un exemple simple. Il permet au robot d'avancer jusqu'à ce que 10 secondes soient passées, ou que le capteur tactile touche quelque chose.

task main()
{
	long t3;
	SetSensor(IN_1,SENSOR_TOUCH);
	t3 = CurrentTick();
	OnFwd(OUT_AC, 75);
	until ((SENSOR_1 == 1) || ((CurrentTick()-t3) > 10000));
	Off(OUT_AC);
}

N'oubliez pas que comme la fonction Wait, les timers fonctionnent à la milliseconde.

Affichage matriciel

La Brique NXT dispose d'un affichage matriciel noir et blanc avec une résolution de 100x64 pixels. Il ya beaucoup de fonctions API pour dessiner des chaînes de caractères, des nombres, points, lignes, rectangles, des cercles, et même des images bitmap (fichiers .ric). L'exemple suivant tente de couvrir tous ces cas. Le pixel numérotées (0,0) est en bas à gauche.

#define X_MAX  99
#define Y_MAX  63
#define X_MID  (X_MAX+1)/2
#define Y_MID  (Y_MAX+1)/2
 
task main(){
	int i = 1234;
	TextOut(15,LCD_LINE1,"Display", true);
	NumOut(60,LCD_LINE1, i);
	PointOut(1,Y_MAX-1);
	PointOut(X_MAX-1,Y_MAX-1);
	PointOut(1,1);
	PointOut(X_MAX-1,1);
	Wait(200);
	RectOut(5,5,90,50);
	Wait(200);
	LineOut(5,5,95,55);
	Wait(200);
	LineOut(5,55,95,5);
	Wait(200);
	CircleOut(X_MID,Y_MID-2,20);
	Wait(800);
	ClearScreen();
	GraphicOut(30,10,"faceclosed.ric");
	Wait(500);
	ClearScreen();
	GraphicOut(30,10,"faceopen.ric");
	Wait(1000);
}

Toutes ces fonctions sont assez explicites, mais maintenant je vais vous décrire leurs paramètres en détail.

- ClearScreen() efface l'écran;
- NumOut(x, y, nombre) vous permet de spécifier les coordonnées et le numéro;
- TextOut(x, y, string) fonctionne comme ci-dessus, mais génère une chaîne de texte
- GraphicOut (x, y, nom de fichier) montre une image bitmap d'un fichier .ric
- CircleOut (x, y, rayon) crée un cercle spécifié par les coordonnées du centre et du rayon;
- LineOut (x1, y1, x2, y2) trace une ligne qui va du point (x1, x2) au point (x2, y2).
- PointOut (x, y) met un point sur ​​l'écran
- RectOut (x, y, largeur, hauteur) dessine un rectangle en bas à gauche avec le vertex (x, y) et avec les dimensions spécifiées;

- ResetScreen () réinitialise l'écran.

Système de fichiers

Le NXT peut écrire et lire des fichiers, stockés dans sa mémoire flash. Ainsi, vous pouvez enregistrer un enregistrement de données à partir des données du capteur ou lire des chiffres lors de l'exécution du programme. La seule limite au nombre de fichiers et la dimension est la taille de la mémoire flash. Les fonctions de l'API NXT vous permettent de gérer les fichiers (créer, renommer, supprimer, trouver), vous permettent de lire et d'écrire des chaînes de texte, des nombres et les octets.

Dans le prochain exemple, nous verrons comment créer un fichier, y écrire des chaînes de caractères et le renommer.

Tout d'abord, le programme supprime les fichiers avec les noms que nous allons utiliser : ce n'est pas une bonne habitude (il faut vérifier l'existence de fichiers, le supprimer manuellement ou automatiquement choisir un autre nom pour notre fichier dans lequel nous travaillerons), mais il n'y a pas de problème dans notre simple cas. Il crée notre fichier par CreateFile("Danny.txt", 512, fileHandle), en précisant le nom, la taille et un lien vers le fichier, où le firmware NXT va écrire un certain nombre pour ses propres utilisations.

Ensuite, il génère des chaînes et les écrit dans le fichier avec WriteLnString(fileHandle, string, bytesWritten), où tous les paramètres doivent être des variables. Enfin, le fichier est fermé et renommé. N'oubliez pas: un fichier doit être fermé avant de commencer une autre opération, si vous avez créé un fichier, vous pouvez y écrire, si vous voulez le lire, vous devez le fermer et l'ouvrir avec OpenFileRead(); pour supprimer / renommer, vous devrez le fermer.

#define OK LDR_SUCCESS
 
task main(){
	byte fileHandle;
	short fileSize;
	short bytesWritten;
	string read;
	string write;
	DeleteFile("Danny.txt");
	DeleteFile("DannySays.txt");
	CreateFile("Danny.txt", 512, fileHandle);
	for(int i=2; i<=10; i++ ){
		write = "NXT is cool ";
		string tmp = NumToStr(i);
		write = StrCat(write,tmp," times!");
		WriteLnString(fileHandle,write, bytesWritten);
	}
	CloseFile(fileHandle);
	RenameFile("Danny.txt","DannySays.txt");
}

Pour voir le résultat, BricxCC -> Tools -> NXT Explorer, récupérez DannySays.txt et observez. Prêt pour le prochain exemple ! Nous allons créer une tableau de caractères ASCII.

task main(){
	byte handle;
	if (CreateFile("ASCII.txt", 2048, handle) == NO_ERR) {
		for (int i=0; i < 256; i++) {
			string s = NumToStr(i);
			int slen = StrLen(s);
			WriteBytes(handle, s, slen);
			WriteLn(handle, i);
		}
		CloseFile(handle);
	}
}

Vraiment simple, ce programme crée le fichier et si aucune erreur n'est survenue, il écrit un nombre entre 0 er 255 (le convertir en chaîne de caractère avant) avec writeBytes (handle, s, slen), qui est une autre façon d'écrire des chaînes sans retour chariot ; puis il écrit le nombre avec WriteLn (handle, valeur) qui ajoute un retour chariot à la fin. Le résultat, que vous pouvez voir en ouvrant ASCII.txt avec un éditeur de texte, est explicable: le nombre écrit sous forme de chaîne est représentée d'une manière lisible par un humain, alors que le nombre est écrit comme valeur hexadécimale à interpréter et présentée comme un code ASCII.

Deux fonctions importantes restent à être montré: ReadLnString pour lire des chaînes à partir de fichiers et Readln pour lire les nombres.

Maintenant, pour l'exemple de la première fonction : la tâche principale appelle la sous-routine CreateRandomFile qui crée un fichier avec des nombres aléatoires dedans (écrit sous forme de chaînes); vous pouvez commenter cette ligne et utiliser un autre fichier texte créé à la main pour cet exemple.

Dans la sous-routine CreateRandomFile nous produisons une quantité prédéfinie de nombres aléatoires, les convertissons en chaîne de caractères et les écrivons dans le fichier.

Le ReadLnString accepte un lien vers le fichier et une variable de chaîne comme arguments: après l'appel, la chaîne contient une ligne de texte et la fonction retourne un code d'erreur, que nous pouvons utiliser pour savoir si la fin du fichier a été atteinte.

#define FILE_LINES 10
 
sub CreateRandomFile(string fname, int lines){
	byte handle;
	string s;
	int bytesWritten;
	DeleteFile(fname);
	int fsize = lines*5;
	// Crée le fichier
	if(CreateFile(fname, fsize, handle) == NO_ERR) {
		int n;
		repeat(FILE_LINES) {
			int n = Random(0xFF);
			s = NumToStr(n);
			WriteLnString(handle,s,bytesWritten);
		}
		CloseFile(handle);
	}
}
 
task main(){
	byte handle;
	int fsize;
	string buf;
	bool eof = false;
	CreateRandomFile("rand.txt",FILE_LINES);
	if(OpenFileRead("rand.txt", fsize, handle) == NO_ERR)
	{
		TextOut(10,LCD_LINE2,"Filesize:");
		NumOut(65,LCD_LINE2,fsize);
		Wait(600);
		until (eof == true)
		{
			if(ReadLnString(handle,buf) != NO_ERR)
				eof = true;
			ClearScreen();
			TextOut(20,LCD_LINE3,buf);
			Wait(500);
		}
	}
	CloseFile(handle);
}

Dans le dernier programme, je vais vous montrer comment lire les numéros d'un fichier. Je profite de l'occasion pour vous donner un petit échantillon de la compilation conditionnelle. Au début du code, il ya une définition qui n'est pas utilisé pour une macro ni pour un alias: nous définissons simplement INT.

Ensuite, il ya une déclaration préprocesseur

#ifdef INT
	...code...
#endif

qui indique simplement au compilateur de compiler le code entre les deux états si l'INT comme cela a été défini précédemment. Donc, si nous définissons INT, la tâche principale à l'intérieur du premier couplet sera compilé et si LONG est défini au lieu de l'INT, la deuxième version sera compilée.

Cette méthode me permet de montrer dans un seul programme comment les deux types int (16 bits) et long (32 bits) peuvent être lus depuis un fichier en appelant la même fonction Readln(handle, val).

Comme auparavant, il accepte un lien vers le fichier et une variable numérique comme arguments, et retourne un code d'erreur.

La fonction va lire 2 octets à partir du fichier si la variable passée en paramètre est déclarée comme int, et lire 4 octets si la variable est de type long. Les variables de type bool peuvent aussi être lues et écrites de la même façon.

#define INT //   INT or LONG
#ifdef INT
 
task main () {
	byte handle, time = 0;
	int n, fsize,len, i;
	int in;
	DeleteFile("int.txt");
	CreateFile("int.txt",4096,handle);
	for (int i = 1000; i<=10000; i+=1000){
		WriteLn(handle,i);
	}
	CloseFile(handle);
	OpenFileRead("int.txt",fsize,handle);
	until (ReadLn(handle,in)!=NO_ERR){
		ClearScreen();
		NumOut(30,LCD_LINE5,in);
		Wait(500);
	}
	CloseFile(handle);
}
#endif
 
#ifdef LONG
task main () {
	byte handle, time = 0;
	int n, fsize,len, i;
	long in;
	DeleteFile("long.txt");
	CreateFile("long.txt",4096,handle);
	for (long i = 100000; i<=1000000; i+=50000){
		WriteLn(handle,i);
	}
	CloseFile(handle);
	OpenFileRead("long.txt",fsize,handle);
	until (ReadLn(handle,in)!=NO_ERR){
		ClearScreen();
		NumOut(30,LCD_LINE5,in);
		Wait(500);
	}
	CloseFile(handle);
}
#endif

Conclusion

Dans ce dernier chapitre, vous avez rencontré les fonctionnalités avancées offertes par NXT: minuterie haute résolution, affichage matriciel et le système de fichiers.

Conclusion et remarques

Si vous avez tout fait sur ce tutoriel, vous pouvez maintenant un expert en NXC. Si vous n'avez rien fait, il est temps de commencer à travailler. Avec de la créativité dans la conception et dans la programmation vous pouvez faire faire au robots Lego des choses incroyables.

Ce tutoriel ne couvre pas tous les aspects de la BricxCC. Il est recommandé de lire le Guide NXC. En outre, NXC est encore en développement, la future version pourrait intégrer des fonctionnalités supplémentaires. De nombreux concepts de programmation ne sont pas traitées dans ce tutoriel. En particulier, nous n'avons pas songé à comprendre le comportement de robots ou d'autres aspects de l'intelligence artificielle.

Il est également possible de piloter un robot Lego directement à partir d'un PC. Cela vous oblige à écrire un programme dans un langage comme C++, Python ou Java. Il est également possible de laisser un programme en exécution dans le NXT et d’interférer à distance avec celui-ci. Une telle combinaison est très puissante. Si vous êtes intéressé par ce mode de programmation, le meilleur outil est la SDK Fantom et les documents Open Source de la section NXTreme du site Lego Mindstorms.

http://mindstorms.lego.com/Overview/NXTreme.aspx

Le web est une source idéale pour avoir des informations supplémentaires. Certains autres points d'informations importantes sont sur ​​Lugnet, le LEGO Users Group Network (non officielle):

http://www.lugnet.com/robotics/nxt

Sources

Outils personnels