Les applications Javascript pour Karotz

Après l’article d’introduction sur les applications Karotz, je vais vous présenter dans cet article les bases du développement d’application embarquée sur le lapin.

Les applications embarquées sont développées en Javascript, je vais donc faire une première partie d’introduction sur le sujet puis la description d’une application complète avec une application de type minuteur.

Développement Javascript

Javascript est un langage de programmation événementiel. Cela signifie que les différents traitements seront exécutés à chaque fois suite à un événement particulier.

Comme événement, il y a des événements temporels (timer) ou des événements sur le lapin directement (tags RFID, oreilles, reconnaissance vocale…).

La gestion de ces événements se fait sous forme de Callback (fonction de retour). Chaque fonction de Callback est définie pour gérer un événement particulier. Cela permettra au moteur Javascript d’évoluer de fonction en fonction tout en gérant l’ensemble des événements qui seront déclenchés.

Il est impératif de ne pas faire de boucle infinie dans votre programme Javascript, sinon le Karotz ne pourra plus gérer aucun événement (y compris le triple clic censé le redémarrer), vous n’aurez plus d’autre choix que de le débrancher pour le redémarrer.

En partant du principe que votre application Javascript est une suite d’opérations qui sont accessibles les unes par rapport aux autres grâces à différents événements, on peut ainsi modéliser n’importe quelle application sous forme d’un automate d’état :

Les opérations A, B, C et D sont des traitements et les flèches sont les événements.

  • Par exemple, disons que A est la lecture d’un son. La flèche entre A et B est l’évènement « fin de lecture du son »
  • B est une phase d’activation de l’interaction avec le RFID.
  • L’étape C est une gestion du TAG rfid détecté
  • L’événement entre B et C est la détection d’un nouveau TAG rfid
  • L’événement de C à B est un timer qui n’est activé que si le nombre de TAG a enregistrer n’est pas atteint
  • Si le nombre de TAG enregistré est atteint, un événement timer fait passer de l’étape C à l’étape D

Grâce à ce type de modélisation, on peut créer facilement du pseudo séquentiel tout en conservant la gestion évènementielle du Javascript. Cela est indispensable pour des applications nécessitant une série d’opérations devant être effectuées dans un ordre précis tout en interagissant avec le lapin (bouton, oreille, rfid…)

Pour en revenir à l’application minuteur que nous allons développer en exemple, voici l’automate d’état qui peut modéliser le principe :

On distingue 8 phases :

  1. Initialisation du Karotz (démarre le Karotz et initialise le premier callback de démarrage)
  2. Phrase de démarrage (la synthèse vocale indique la règle de programmation du minuteur)
  3. Activation de la gestion des clics (à partir de cette étape, chaque clic va incrémenter le temps du minuteur)
  4. Incrémentation du temps (chaque clic incrémente le temps de 10 secondes)
  5. Prise en compte du temps à décompter (après un certains temps sans clic, on prend en compte le temps et on lance le décompte)
  6. 5 dernières secondes (5 secondes avant la fin, on fait des bip indiquant la fin du décompte)
  7. Dring… Fin du temps (fin du chronomètre validé par un son)
  8. Arrêt de l’application (après la lecture du son, l’application s’arrête automatiquement)

Ces différentes tâches sont liées entre elles par 9 événements :

  1. Callback de démarrage au lancement du Karotz.
  2. Fin de lecture de la phrase d’introduction.
  3. Timer qui déclenche l’enregistrement des clics pour fixer la durée du décompte.
  4. Chaque clic sur le bouton incrémente le temps de 10sec. On peut ajouter une étape qui annoncera le temps courant.
    A chaque clic, on déclenche aussi un timer de 10sec qui ferra passer à l’étape 5. Si l’utilisateur clique une nouvelle fois sur le bouton, le timer est automatiquement annulé.
  5. Cet événement correspond au timer de 10sec si aucun clic n’est effectué pendant cette durée.
  6. L’événement 6 est un timer correspondant au temps total moins 5 secondes.
  7. Pour chaque seconde des 5 dernières un petit bip permet d’annoncer la fin du décompte.
  8. Pour la dernière seconde, l’événement 8 passe à l’étape qui lira le son indiquant la fin du chronomètre.
  9. Le dernier événement est déclenché dès la fin de la lecture du son final.

Fonctions de gestion de l’automate d’état

Pour gérer l’automate d’état, il faut une fonction globale avec en paramètre le numéro d’étape. Un switch dans cette fonction permettra de gérer les différentes actions par rapport au numéro d’étape.

Ensuite, il faut une fonction qui déclenche un timer et appelle automatiquement la fonction générale au bout d’un certain temps en passant le numéro d’étape en paramètre.

Cette fonction sera la fonction de base de notre système : execAction
Au bout du temps configuré, la fonction automate(num) est appelé avec le numéro d’étape correspondant…

Voila comment passer à l’étape 2 au bout de 2 secondes : execAction(2, 2000);

En plus de la fonction d’exécution d’une action, on ajoute une fonction stopAction qui permettra d’annuler une action.

Le principe est simple, une chaîne de caractère contient la liste des actions à annuler et lorsque l’action doit être exécutée, on vérifie si elle n’est pas dans cette liste d’action annulée…

// Chaque lancement d'action est identifié de manière unique
var idAction = 0;

// On utilise une chaîne pour marquer les actions qui doivent être annulée
// Pour chaque action à annuler, on ajoute #idAction# dans la chaîne
var abortedActions = ""; 

///////////////////////////////////////////////////////////////////////////////////////
/// FONCTIONS DE GESTION D'UN AUTOMATE D'ETAT
///////////////////////////////////////////////////////////////////////////////////////
//  Les fonctions suivantes permettent la gestion d'un automate d'état.
//  Dans votre code, il faut créer une fonction de gestion de l'automate:
//          function automate(num) {
//              switch (num) {
//              case 0:
//                  // Fonction à l'étape 0
//              break;
//              case 1:
//                  // Fonction à l'étape 1
//              break;
//              }
//          }
//
///////////////////////////////////////////////////////////////////////////////////////
// Dans le cas d'utilisation d'un automate d'état, cette fonction
// permet de passer à l'étape suivante (num) en faisant une pause de
// durée delay avant.
// La fonction renvoi l'id du Timer permettant le passage à l'étape
// suivante. Cela permettra éventuellement d'interrompre l'opération
// en utilisant le stopAction
function execAction(num, delay)
{
    idAction++;

    var id = idAction;

    // Défini un timeout qui permettra de passer à l'étape suivante après le temps delay
    setTimeout(delay, function()
                      {
                          // On contrôle si l'action n'a pas été annulée
                          if (abortedActions.indexOf("#"+id+"#")!=-1)
                            abortedActions = abortedActions.replace("#"+id+"#", "");
                          else
                            automate(num); 

                          return false;
                      });

    return idAction;
}

///////////////////////////////////////////////////////////////////////////////////////
// Interromp le passage à l'étape suivante dans le cas d'un automate d'état
// L'id passé en paramètre doit être celui retourné par la fonction execAction
// Cet id correspond au timer utilisé pour passé entre chaque étape...
function stopAction(idAction)
{
    abortedActions += "#"+idAction+"#";
}

Afin de pouvoir traiter automatiquement les opérations de lecture multimédia ou synthèse vocale, on peut créer des fonctions qui appelleront automatiquement la fonction execAction à la fin du traitement (cf code ci-dessous) :

///////////////////////////////////////////////////////////////////////////////////////
// La fonction speakAndGoto permet de prononcer un text en TTS, puis d'aller à l'étape
// num de l'automate d'état. Il est possible de spécifier un delais (delay)
// entre la fin de la prononciation du texte et le passage à l'étape suivante.
function speakAndGoto(txt, num, delay)
{
  karotz.tts.start(txt, "fr", function(event) {
                                  if (event=="TERMINATED")
                                  {
                                      execAction(num, delay);
                                  }

                                  return true;
                              });
}

///////////////////////////////////////////////////////////////////////////////////////
// La fonction playAndGoto permet de lire un fichier multimedia, puis d'aller à l'étape
// num de l'automate d'état. Il est possible de spécifier un delais (delay)
// entre la fin de la prononciation du texte et le passage à l'étape suivante.
function playAndGoto(url, num, delay)
{
  karotz.multimedia.play(url, function(event) {
                                  if (event=="TERMINATED")
                                  {
                                      execAction(num, delay);
                                  }

                                  return true;
                              });
}

Toutes ces fonctions ont été réunies dans un fichier Javascript que vous pouvez télécharger ici : planetedomo.js (Ce lien est désormais désactivé)

Vous pourrez ensuite très facilement utiliser les fonctions dans un programme ayant la structure suivante :

include("planetedomo.js");

function automate(num)
{
    switch (num)
    {
    case 1:
    break;
    case 2:
    break;
    }
}

var onKarotzConnect = function(data)
{
 execAction(1, 0);
}

karotz.connectAndStart(karotz_ip, 9123, onKarotzConnect, {});

Revenons en à notre programme minuteur en définissant le code des différentes étapes…

Le Karotz minuteur :

Comme vu dans le précédent article, il nous faut d’abord définir le screen.xml et le descriptor.xml

Pour le screen.xml, rien de particulier, l’application pourra être lancé avec un TAG rfid ou par reconnaissance ASR (Automatic Speech Recognition) :

<screen  nanoTrigger="true"
         permanentTrigger="false"
         scheduledTrigger="false"
         scheduledDateTrigger="false"
         voiceTrigger="true">
</screen>

Pour le descriptor.xml, il faut activer les services qui seront utilisés (button, tts) et indiquer que c’est une application hosted :

<descriptor>
    <name>K-Minute</name>
    <version>1.0.0</version>
    <accesses>
       <access>button</access>
       <access>tts</access>
    </accesses>
    <editor>PlanèteDomo</editor>
    <deployment>hosted</deployment>
    <interruptible>false</interruptible>
    <awake>false</awake>
    <callback/>
    <multiConf>false</multiConf>
</descriptor>

Le bouton sera utilisé à la fois pour arrêter l’application et pour programmer le temps à décompter.

Chaque fois que l’utilisateur appuiera sur le bouton le temps total s’incrémente de 10 secondes.

Le tts (synthèse vocale) sera utilisé pour décrire le principe (au lancement de l’application), indiquer le temps courant et annoncer la fin du décompte.

include("util.js");
include("planetedomo.js");

var karotz_ip="localhost";

var idtimer = -1;
var enableBtn = false;
var timertotal = 0;
var lastfivesec = 5;

var buttonListener = function(event)
{
    if (event == "SIMPLE" && enableBtn==true)
    {
        // on incrémente le timer de 10sec
        timertotal+=10;
        execAction(4, 0);
    }

    // On désactive le double clic pendant la programmation pour éviter les fausses manip
    if (event == "DOUBLE" && enableBtn==false)
    {
        execAction(8, 0);
    }
    return true;
}

function automate(num)
{
   switch (num)
   {
   case 2:
      // 2. Phrase de démarrage (la synthèse vocale indique la règle de programmation du minuteur)
      speakAndGoto("Pour configurer le minuteur, appuyez sur le bouton sur la tête de karotz pour chaque tranche de 10 secondes", 3, 0);
      enableBtn = true;
      timertotal = 0;
   break;

   case 3:
       // 3. Activation de la gestion des clics (à partir de cette étape, chaque clic va incrémenter le temps du minuteur)
       // On supprime le timer de 10s précédent qui déclenche la prise en compte du temps
      if (idAction>=0)
          stopAction(idAction);

      if (timertotal>0)
          idAction = execAction(5, 10000);

   break;

   case 4:
       // 4. Incrémentation du temps (chaque clic incrémente le temps de 10 secondes)
      speakAndGoto(timertotal + " secondes", 3, 0);

   break;

   case 5:
       // 5. Prise en compte du temps à décompter (après un certains temps sans clic, on prend en compte le temps et on lance le décompte)
      enableBtn = false;
      speakAndGoto("Démarrage minuteur : " + timertotal + " secondes", 6, (timertotal-5)*1000);
   break;

   case 6:
       // 6. 5 dernières secondes (5 secondes avant la fin, on fait des bip indiquant la fin du décompte)
      if (lastfivesec==0)
          execAction(7, 1000);
      else
          speakAndGoto("bip", 6, 1000);

      lastfivesec-=1;
   break;

   case 7:
        // 7. Dring… Fin du temps (fin du chronomètre validé par un son)
        speakAndGoto("Driiiiiiiiiiiiiiiinng!", 8, 0);
   break;

   case 8:
        // 8. Arrêt de l’application (après la lecture du son, l’application s’arrête automatiquement)
       karotz.tts.stop();
       exit();
    break;
    }
}

var onKarotzConnect = function(data)
{
    karotz.button.addListener(buttonListener);
    execAction(2, 0);
}

// 1. Initialisation du Karotz (démarre le Karotz et initialise le premier callback de démarrage)
karotz.connectAndStart(karotz_ip, 9123, onKarotzConnect, {});

Le principe du déclenchement du minuteur est d’attendre 10 secondes après le dernier appui sur le bouton. Le décompte se lance alors automatiquement.

Pour améliorer un peu l’application, on va ajouter des sons à la place des mots prononcés à la fin du timing… et on va utiliser la led pour les différentes phases.

Il faut d’abord modifier le descriptor.xml pour autoriser l’utilisation de la led et des sons (multimedia) :

<descriptor>
    <name>K-Minute</name>
    <version>1.0.0</version>
    <accesses>
       <access>button</access>
       <access>tts</access>
       <access>led</access>
       <access>multimedia</access>
    </accesses>
    <editor>PlanèteDomo</editor>
    <deployment>hosted</deployment>
    <interruptible>false</interruptible>
    <awake>false</awake>
    <callback/>
    <multiConf>false</multiConf>
</descriptor>

En utilisant des bruitages du site https://www.universal-soundbank.com voila ce que peut être l’application finale :

var karotz_ip="192.168.1.35";

var idAction = -1;
var enableBtn = false;
var timertotal = 0;
var lastfivesec = 4;

var buttonListener = function(event)
{
    if (event == "SIMPLE" && enableBtn==true)
    {
        // On va à l'étape 4 après avoir incrémenté le timer de 10sec
        timertotal+=10;
        execAction(4, 0);
    }

    // On désactive le double clic pendant la programmation pour éviter les fausses manip
    if (event == "DOUBLE" && enableBtn==false)
    {
        // Sinon, le double clic permet de sortir de l'application
        execAction(8, 0);
    }
    return true;
}

function automate(num)
{
    switch (num)
    {
    case 2:
      // 2. Phrase de démarrage (la synthèse vocale indique la règle de programmation du minuteur)
      speakAndGoto("Pour configurer le minuteur, appuyez sur le bouton sur la tête de karotz pour chaque tranche de 10 secondes", 3, 0);
      karotz.led.light("00FF00");
      enableBtn = true;
      timertotal = 0;
      break;

    case 3:
      // 3. Activation de la gestion des clics (à partir de cette étape, chaque clic va incrémenter le temps du minuteur)
      // On supprime le timer de 10s précédent qui déclenche la prise en compte du temps
      if (idAction>=0)
        stopAction(idAction);

      if (timertotal>0)
        idAction = execAction(5, 10000);

      break;

    case 4:
      // 4. Incrémentation du temps (chaque clic incrémente le temps de 10 secondes)
      speakAndGoto(timertotal + " secondes", 3, 0);

      break;

    case 5:
      // 5. Prise en compte du temps à décompter (après un certains temps sans clic, on prend en compte le temps et on lance le décompte)
        enableBtn = false;
        speakAndGoto("Démarrage minuteur : " + timertotal + " secondes", 6, (timertotal-5)*1000);
        karotz.led.light("0000FF");
      break;

    case 6:
      // 6. 5 dernières secondes (5 secondes avant la fin, on fait des bip indiquant la fin du décompte)
        if (lastfivesec==0)
          execAction(7, 500);
        else
          playAndGoto("https://www.universal-soundbank.com/mp3/sounds/1933.mp3", 6, 500);

        if (lastfivesec==4)
        {
          karotz.led.light("CC9900");
          karotz.led.pulse("AAAAAA", 200, -1);
        }
        lastfivesec-=1;
      break;

    case 7:
      karotz.led.light("CC9900");

      // 7. Dring… Fin du temps (fin du chronomètre validé par un son)
        playAndGoto("https://www.universal-soundbank.com/mp3/sounds/1451.mp3", 8, 0);
      break;

    case 8:
      // 8. Arrêt de l’application (après la lecture du son, l’application s’arrête automatiquement)
      karotz.tts.stop();
      exit();
      break;
    }
}

var onKarotzConnect = function(data)
{
    karotz.button.addListener(buttonListener);
    execAction(2, 0);
}

Voila une petite application sans prétention qui vous décrit rapidement les principes de base d’une application Karotz.

Le développement d’application Javascript sur Karotz sous forme d’automate d’état est bien sûr ma vision personnel des choses, il existe bien d’autres moyens de faire… N’hésitez pas à donner votre avis dans les commentaires…

Vous pouvez télécharger l’application complète ici : k-minute.zip (Ce lien est désormais désactivé)

Et n’oubliez pas que Karotz et ses accessoires sont disponibles dans ma boutique : Karotz & Accessoires

A propos Mickael

Je suis passionné de Domotique depuis des années. J'ai fait du développement en informatique industrielle pendant 12 ans, et un jour ... je me suis lancé ! J'ai créé ma boutique de vente en ligne : https://www.planete-domotique.com

A voir aussi

Une Zibase Multi pour quoi faire ?

La société Zodianet vient juste de sortir une nouvelle version de sa célèbre Zibase, un …

5 commentaires

  1. Très clair, très complet.
    On aurait pu aussi passer des pointeurs de fonctions (1 fct par étape de l’automate) et se passer de la fonction d’aiguillage qui devient rapidement pesante sur une grosse appli. C’est à dire que le cas 1 devient etape1 et au lieu de passer 1 en avant dernier parametre de PlayAndGo passer etape1. Ceci permettrait aussi que certaines étapes soient des méthodes d’objets qui encapsule des donnés et éviter de tout mettre en global ?

    • Bonjour Bruno,
      Effectivement, j’aurais pu passer des callback. Mais je viens du milieu de l’automatisme (informatique industrielle), donc j’ai plus l’habitude de faire des automates d’états avec un système de switch et un numéro d’étape 🙂
      D’ailleurs souvent j’utilise une numérotation d’étape plus du type 10, 20, 30 … ce qui permet de conserver des réserves en cas d’ajout d’étape intermédiaire 😉

  2. Merci pour ces précision qui m’ont beaucoup aidé. J’avais dans l’idée de développer un minuteur aussi et en tombant sur ta page je me suis que j’allais tester ton fichier. Seulement quand j’upload le fichier zip sur karotz app il me mets que le descriptor.xml est inexistant ou inadequate. Une idée d’oú vient le problème? J’ai tenté de changer le nom de l’application et de simplifier les paramètres en vain….

  3. @JC : Je viens de retester, ça fonctionne bien.

    Attention, si tu rezip mon appli, il faut impérativement que les fichiers soient à la racine du zip. Si tu les met dans un répertoire, ça ne marche pas …

  4. ok merci ca marche maintenant, j’avais mal rezippé apparamment et oublier ce changer la version. Maintenant petit problème comment il marche ce programme? Car mon karotz ne veut plus s’arrêter, il est en orange pulse continu… Merci d’avance

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *