Ecouter le Port Série ou un keypad (applicable à 1 flux de données asynchrone )

Salut à toutes et tous

Je vois passer tellement de questions concernant la gestion d'un port série ou la lecture du keypad (ou d'une réponse à une requête GET HTTP) que je me dis qu'un petit tuto sur ce sujet ne peut pas faire de mal :slight_smile:

Ce tuto n'a pas vocation à expliquer en détail le port série et les bases de la communication. Ceci est très bien fait dans les tutos d'eskimon: Communication par la voie série

Ce tuto n'a pas vocation à détailler l'implémentation d'un protocole de communication, il y a déjà un bon tuto de barbudor sur ce sujet en français

Enfin - ce tuto n'a pas vocation à remplacer le tuto de Robin en anglais Serial Input Basics auquel je fais souvent référence et qui est très complet. Vous retrouverez des principes généraux du tuto de Robin, mais en français et en se concentrant sur deux ou trois trucs pratiques.


Quelques bases: String ou c-string

Comme le dit la doc Arduino, il existe deux façons de représenter des chaînes de caractères. La classe String (avec un grand S) et le tableau de caractère terminé par un caractère nul '\0', que l'on appelle c-string car venant du langage C.

Ce tutorial n'utilisera pas la classe String. Cette classe n'est pas l'amie du programmeur sur petits micro-contrôleurs quand on ne comprend pas bien les notions d'allocation de mémoire dynamique.

Bien que les nouveaux Arduino à base ARM et similaires aient plus de RAM, on reste très loin des Giga-Octets de RAM dispos sur un ordinateur et il n'y a pas d'OS pour faire le ménage dans la mémoire (garbage collector) si d'aventure une allocation dynamique venait à ne pas trouver un bloc contigu de mémoire disponible.

Je vous laisse chercher sur internet la notion de pile et tas (éventuellement en commençant par ce post en français qui a été copié sur ce post plus détaillé en anglais du site Adafruit)

Vous pouvez aussi lire cet article en anglais The Evils of Arduino Strings qui explique très bien pourquoi cette classe vous jouera sans doute des tours.

Alors oui, pour les jeunes qui n'ont connu que les langages modernes ou interprétés sur gros ordinateurs, on retournera un peu à l'âge de pierre, plus près du matériel et des octets, et on n'utilisera pas des fonctions avancées de la classe String qui - au coût d'une utilisation mémoire intensive et un risque de bugs difficiles à trouver - vont vous simplifier un peu la vie..

à titre d'exemple qui je l'espère vous convaincra - ce code qui affiche Bonjour tout le monde dans la console

String message = "Bonjour tout le monde";

void setup() {
  Serial.begin(115200);
  Serial.print(message);
}

void loop() {}

occupera sur un UNO 2848 octets de mémoire soit 8% de l'espace de stockage de programmes et 222 octets (10%) de mémoire dynamique tout simplement parce que j'ai choisi la classe String pour stocker mon message.

Comparez cela à ce programme qui fait la même chose

char message[] = "Bonjour tout le monde";

void setup() {
  Serial.begin(115200);
  Serial.print(message);
}

void loop() {}

Ce croquis utilise 1482 octets soit 4% de l'espace de stockage de programmes et 206 octets (10%) de mémoire dynamique.

Donc sans parler des soucis de tas et de pile, on voit donc pleinement l'impact de la classe String sur la mémoire programme (quasiment 1400 octets de plus!) et donc si on peut s'en passer c'est mieux surtout si vous avez besoin de place pour votre code perso.

Mais comment alors effectuer des opérations un peu avancées sur les c-string de façon à ne pas à avoir à complètement réinventer la roue? il y a pour cela nombre de fonctions provenant du langage C qui savent travailler sur les c-string. Vous les trouverez dans les librairies standards stdlib.h et string.h par exemple.

C'est ce que j'utiliserai dans ce tutorial.

1 Like

Asynchrone...

L’asynchronisme désigne le caractère de ce qui ne se passe pas à la même vitesse, que ce soit dans le temps ou dans la vitesse proprement dite, par opposition à un phénomène synchrone

(source wikipedia)

Lors de la gestion d'un flux de communication asynchrone, les données que vous allez recevoir ne vont pas arriver en un flot continu avec un timing parfait. C'est simple à comprendre avec un keypad: l'utilisateur va appuyer sur les touches au rythme qui lui convient et c'est à votre code de s'adapter à ce rythme, pas à l'utilisateur de taper à la microsondes près au rythme qu'attendrait votre code.

Quand c'est asynchrone, il ne faut pas essayer de deviner avec une attente statique quand la prochaine entrée va arriver mais il faut que le code aille voir fréquemment s'il y a quelque chose en attente et si c'est le cas lire cette information et la traiter.

Par exemple, vous voulez lire un message sur le port série. vous utilisez [

Serial.available()

](Serial.available() - Arduino Reference) pour vérifier si quelque chose est dans le buffer d'entrée, puis vous vous dites je vais attendre un peu que tout soit arrivé et je vais lire le résultat. Vous seriez donc tenté de rajouter un petit delay(10); // on attend le message puis de faire une petite boucle de lecture

  while (Serial.available()) {
    int r = Serial.read();
  }

en pensant que ça va toujours marcher pour lire le message. (on voit ça très souvent)

Le souci de cette approche c'est que les données peuvent arriver plus ou moins vite dans le petit buffer Série de votre Arduino - et même en supposant qu'un message ait été envoyé d'un bloc par l'ordinateur distant, la vitesse d'arrivée des octets dépend d'un certain nombres de facteurs et notamment de la vitesse en bauds de la ligne de communication.

Sans rentrer dans les détails, en gros envoyer un octet correspond à environ 10 bits à transmettre en général. ça veut dire qu'à 9600 bauds, vous envoyez 960 octets par seconde et à 115200 bauds vous envoyez 11520 octets par seconde. On voit bien que si vous avez effectué un delay(10); pour attendre le message (soit 1 centième de secondes) à 9600 bauds votre buffer aura reçu 9 ou 10 caractères et qu'à 115200 bauds il en aura éventuellement reçu environ 115.

Votre boucle while qui va ensuite lire les données reçues va s'exécuter super vite, en quelques dizaines de microsondes, et va donc vider intégralement le buffer du port Série avant même généralement de pouvoir recevoir le prochain caractère. Cela veut dire qu'à 9600 bauds si votre message fait plus de 9 caractères et que vous avez mis delay(10);, la boucle while ne lira pas tout le message (Serial.available() dira qu'il n'y a plus rien à lire si vous l'appelez et si vous essayez de lire plus le Serial.read() va retourner -1) et à 115200 bauds, si d'aventure le message fait plus de 64 octets - comme le buffer série est limité à 64 octets sur les Arduino type UNO - vous aurez reçus jusqu'à 115 caractères et donc vous aurez perdu la moitié du message...

Si vous voulez construire un code robuste qui s'adapte à des conditions diverses (bauds, comportement de l'utilisateur, ...) il faut bannir l'utilisation du delay() qui bloque votre programme, ne pas essayer de deviner quand vont arriver les octets et approcher le problème différemment: votre micro-contrôleur fait très bien une chose qui ennuierait tout humain: il peut demander sans se lasser des milliers de fois par secondes "dis papa quand est-ce qu'on arrive?" et le papa lui répondra toujours très courtoisement avec l'information voulue :)...

Dans notre cas on va donc essayer d'aller voir de temps en temps - le plus souvent possible - s'il y a quelque chose en attente pour nous et si oui le traiter, sinon on va faire autre chose.

C'est un peu l'idée du fonctionnement de la loop() de vos croquis Arduino. On boucle en faisant un certain nombre de choses non bloquantes pas trop longtemps de manière à avoir un système réactif (cf mon tuto sur les machine à états éventuellement qui adopte un principe similaire).

La notion de marqueurs

Quand on échange de l'information il faut généralement pouvoir savoir où commence un message et où il se termine.

Dans la vraie vie, quand vous discutez avec quelqu'un au téléphone par exemple, vous entendez un son et vous savez donc que votre interlocuteur a commencé à parler. Vous écoutez et vous répondez.

Si je vous dis:

"Bonjour" "comment ça va ?"

vous avez la possibilité si le premier silence est long, de déterminer que j'ai fini de vous saluer et vous me répondez "Salut", puis vous entendez la seconde partie et vous me répondez "ça va pas mal merci et vous ?"... on aura donc

- "Bonjour"
- "Salut"
- "Comment ça va ?"
- "ça va pas mal merci et vous ?"

si le premier silence est court la discussion sera plutôt

- "Bonjour, Comment ça va ?"
- "Salut, ça va pas mal merci et vous ?"

et si vous êtes malpoli vous pouvez même me répondre pendant que je parle :slight_smile:

Ce qu'il se passe c'est que vous interprétez au fur et à mesure (car votre cerveau bosse) ce que votre interlocuteur dit et quand vous avez compris - où quand il se tait pendant un certain de temps (arbitraire est-ce une pause pour respirer et la phrase n'est pas finie - où a-t-il fini de s'exprimer) - vous avez un message complet et envoyez une réponse ou effectuez une action.

- "Va ranger ta chambre et fais tes devoirs !"
- "Oui papa, bien sûr, tout de suite." + action (enfin pas toujours :slight_smile: )

ça c'est la vraie vie et ça marche pas trop mal car votre cerveau est plutôt puissant et même si deux messages sonores sont mêlées et arrivent à vos oreilles, votre cerveau saura généralement les différencier (la radio et quelqu'un qui vous parle = aucun soucis pour les humains)... Mais pour aider nos petits micro-contrôleurs peu puissant à déchiffrer des ordres ou recevoir des messages, on doit éviter la pollution sonore (bruit sur la ligne) et les aider un peu en utilisant différentes techniques pour leur signaler des choses, du genre:

"Et fais gaffe un peu, je vais te parler" --> marqueur de début de communication
"C'est bon j'ai finis de parler, tu as toute l'info" --> marqueur de fin de communication

Sur un keypad, le "j'ai quelque chose à dire" c'est plusieurs pins qui vont prendre un niveau logique donné pour indiquer quelle touche est appuyée par exemple. le composant port série matériel gère cela aussi au niveau bas pour la gestion de l'arrivée des bits et avec la classe Serial va vous présenter un octet complet une fois reçu.


La classe [url=https://www.arduino.cc/en/Reference/Serial]Serial[/url] (ou Stream en fait) offre des fonctions de haut niveau pour lire des données. Par exemple vous pouvez faire un [url=https://www.arduino.cc/en/Reference/ParseInt]Serial.parseInt()[/url] pour lire un entier.

Cette fonction embarque un peu d'intelligence "humaine" comme lors d'une conversation. Cette fonction écoute, pendant un certain temps, ce qui se dit sur le port série et ignore tout ce qui ne ressemble pas à un chiffre ou un tiret "moins", puis une fois trouvé ce marqueur de début va tenter d'interpréter un entier et arrêter soit au prochain caractère qui ne sera pas un symbole ASCII de chiffre soit attendre un time out (paramétrable). Par exemple si vous envoyez sur le port série [color=red]bonjour[/color][color=blue]1234[/color][color=purple]coucou[/color]

la fonction va laisser tomber la partie [color=red]bonjour[/color], fabriquer le nombre 1234 en lisant [color=blue]1234[/color] sur le port série et s'arrêter en laissant dans le buffer série [color=purple]coucou[/color] puisque la fonction aura reconnu le [color=purple][b]c[/b][/color] comme n'étant pas un chiffre.

mais si vous tapez juste [color=red]bonjour[/color][color=blue]1234[/color] alors la fonction va ignorer la partie [color=red]bonjour[/color], fabriquer le nombre 1234 en lisant [color=blue]1234[/color] sur le port série mais va devoir attendre le timeout (1 seconde par défaut) car elle n'a aucun moyen de savoir si le nombre est arrivé complètement ou pas - et 1 seconde c'est long parfois!

C'est un peu comme dans la vraie vie si je vous demande "combien ça coûte" et que vous dites lentement "mille deux..... cent .... trois euros", tant que je n'ai pas tout entendu je ne sais pas si c'est "1002" ou '1200" ou finalement "1203" car le mot "euros" et mon marqueur de fin de réponse. Cependant vous auriez pu rajouter "et trente deux centimes" après le mot clé, donc tout cela reste un peu arbitraire et on voit bien que pour des raisons de simplicité, avoir un marqueur de début et surtout un marqueur de fin ça aide à bien se comprendre (surtout si on a une intelligence limitée comme nos petits arduinos)

Passons aux choses sérieuses: un peu de code :slight_smile:

Très souvent lorsqu'on lit un texte la structure que l'on regarde c'est la phrase, elle se termine par un point et si c'est un paragraphe un retour à la ligne. Ce sont des marqueurs et on retrouve en informatique cette notion de commandes par ligne.

Un passage à la ligne en informatique est hérité des machines à écrire. Il y a deux ordres: un ordre qui dit "sauter une ligne" (Line Feed) et un ordre qui demandait au charriot de la machine à écrire (ou de l'imprimante) de se repositionner tout au début de la ligne, de revenir vers la marge à gauche.

le saut de ligne est le caractère ASCII Line Feed '\n' (dit LF ou New Line NL pour nouvelle ligne) et le retour à gauche et le caractère ASCII Carriage Return (dit CR) '\r'

Plus d'infos sur wikipedia

Dans la console Série de l'IDE Arduino, vous avez une ligne en haut pour envoyer de l'information sur le port Série vers votre Arduino et en bas de cette fenêtre, vous avez la possibilité de définir quoi mettre comme marqueur de fin de ligne:

CRLF.png

quand vous validez votre ligne, suivant le choix dans le popup, vous allez envoyer rien du tout, juste '\n', juste '\r' ou alors les deux '\r' suivi de '\n'

Voici un peu de code pour voir ce qu'il se passe:

void ecouter()
{
  if (Serial.available()) {
    // on a au moins 1 octet en attente
    int c = Serial.read(); // on lit la valeur. la fonction read retourne un entier: -1 en cas d'erreur sinon l'octet lu (dans l'octet de poids faible de l'entier)
    if (c != -1) { // s'il n'y a pas eu d'erreur de lecture
      Serial.print(F("Octet lu: 0x")); Serial.print(c, HEX); // ici c est un entier, on affiche le code ASCII en Hexadécimal
      switch (c) {
        case '\n':
          Serial.println(F(" caractere: [\\n]"));
          break;
        case '\r':
          Serial.println(F(" caractere: [\\r]"));
          break;
        default:
          Serial.print(F(" caractere: [")); Serial.print((char) c); Serial.println(F("]"));
          break;
      }
    }
  }
}

void setup() {
  Serial.begin(115200);
}

void loop() {
  ecouter();

 // ici on peut faire autre chose

}

Si vous tapez Bonjour dans la console en ayant choisi d'avoir les 2 marqueurs vous verrez cela:


(edit: désolé pour la faute d'orthographe dans l'image...)

si vous avez un clavier type membrane, un code équivalent pourrait être

#include <Keypad.h>

const byte ROWS = 4; //4 rangées
const byte COLS = 4; //4 colonnes

char keys[ROWS][COLS] = {   //les symboles, déclarés par leur code ASCII dans le tableau à 2 dimension
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'}
};

byte rowPins[ROWS] = { 34, 36, 38, 40 }; // On connecte ROW0, ROW1, ROW2 and ROW3 à ces pins (je suis sur un MEGA)
byte colPins[COLS] = {41, 39, 37, 35}; // On connecte  COL0, COL1, COL2 and COL3 à ces pins (je suis sur un MEGA)

Keypad membraneKeypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS);  //On initialise une instance de la classe Keypad

void ecouter()
{
  char c = membraneKeypad.getKey();
  if (c != NO_KEY) {
    Serial.print(F("Octet lu: 0x")); Serial.print((byte) c, HEX); // on convertit c en entier, on affiche le code ASCII en Hexadécimal
    Serial.print(F(" caractere: [")); Serial.print(c); Serial.println(F("]"));
  }
}

void setup() {
  Serial.begin(115200);
}

void loop() {
  ecouter();

  // ici on peut faire autre chose

}

et si j'appuie les touches 1, 2, 3, et * sur le clavier à membrane j'aurais dans la console

[sub][color=blue][nobbc]Octet lu: 0x31 caractere: [1]
Octet lu: 0x32 caractere: [2]
Octet lu: 0x33 caractere: [3]
Octet lu: 0x2A caractere: [*]
[/nobbc][/color][/sub]
1 Like

voilà - mine de rien on a tout ce qu'il nous faut pour travailler. une fonction dans la boucle attend une action utilisateur et la capture au plus vite. le reste ne dépend plus que de vous. Généralement on veut construire un chaine de caractère tant qu'on n'a pas reçu le marqueur de fin de chaîne et lorsqu'on a notre message on voudra effectuer une action.

Pour cela on va modifier un peu la fonction ecouter() pour qu'elle construise la chaine de caractères petit à petit, et qu'elle nous dise si oui ou non on est encore en attente de notre marqueur de fin.

Pour simplifier on va travailler avec un buffer global qui mémorise tout ce qu'on tape. Bien sûr il aura une taille limitée, à vous de le dimensionner correctement en fonction de ce que vous attendez et dans le code d'éviter un débordement de tableau. La tactique adoptée ici c'est que toutes les lettres reçues après la fin du tableau sont ignorées et on ne conserve que le début du message.

Voici un petit code à essayer.

const byte tailleMessageMax = 50;
char message[tailleMessageMax + 1]; // +1 car on doit avoir un caractère de fin de chaîne en C, le '\0'

const char marqueurDeFin = '#';

boolean ecouter()
{
  static byte indexMessage = 0; // static pour se souvenir de cette variable entre 2 appels consécutifs. initialisée qu'une seule fois.
  boolean messageEnCours = true;

  while (Serial.available() && messageEnCours) {
    int c = Serial.read();
    if (c != -1) {
      Serial.print(F("Octet lu: 0x")); Serial.print(c, HEX); // ici c est un entier, on affiche le code ASCII en Hexadécimal
      Serial.print(F("\t[")); Serial.print((char) c); Serial.println(F("]"));
      switch (c) {
        case marqueurDeFin:
          Serial.println(F("Fin de chaine"));
          message[indexMessage] = '\0'; // on termine la c-string
          indexMessage = 0; // on se remet au début pour la prochaine fois
          messageEnCours = false;
          break;
        default:
          if (indexMessage <= tailleMessageMax - 1) message[indexMessage++] = (char) c; // on stocke le caractère et on passe à la case suivante
          else Serial.println(F("j'ignore!"));
          break;
      }
    }
  }
  return messageEnCours;
}

void setup() {
  Serial.begin(115200);
}

void loop() {
  if (! ecouter()) {
    // on a reçu le marqueur de fin
    Serial.print(F("Phrase: [")); Serial.print(message); Serial.println(F("]"));
  }

  // ici on peut faire autre chose

}

Le marqueur de fin de chaîne a été mis à '#' dans le code, donc si vous tapez coucou# le tableau de caractère message va contenir le mot "coucou" (enlevez le CR LF dans la console pour éviter de recevoir aussi ces caractères qui ne s'impriment pas. On pourrait aussi dire que l'on veut '\n' comme marqueur de fin)

de la même façon avec un clavier à membrane, on construit notre message au fil de l'eau

#include <Keypad.h>

const byte ROWS = 4; //4 rangées
const byte COLS = 4; //4 colonnes

char keys[ROWS][COLS] = {   //les symboles, déclarés par leur code ASCII dans le tableau à 2 dimension
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'}
};

byte rowPins[ROWS] = { 34, 36, 38, 40 }; // On connecte ROW0, ROW1, ROW2 and ROW3 à ces pins (je suis sur un MEGA)
byte colPins[COLS] = {41, 39, 37, 35}; // On connecte  COL0, COL1, COL2 and COL3 à ces pins (je suis sur un MEGA)

Keypad membraneKeypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS);  //On initialise une instance de la classe Keypad

const byte tailleMessageMax = 50;
char message[tailleMessageMax + 1]; // +1 car on doit avoir un caractère de fin de chaîne en C, le '\0'

const char marqueurDeFin = '#';


boolean ecouter()
{
  static byte indexMessage = 0; // static pour se souvenir de cette variable entre 2 appels consécutifs. initialisée qu'une seule fois.
  boolean messageEnCours = true;

  char c = membraneKeypad.getKey();
  if (c != NO_KEY) {
    Serial.print(F("Octet lu: 0x")); Serial.print((byte) c, HEX); // on convertit c en entier, on affiche le code ASCII en Hexadécimal
    Serial.print(F(" caractere: [")); Serial.print(c); Serial.println(F("]"));
    if (c == marqueurDeFin) {
      Serial.println(F("Fin de chaine"));
      message[indexMessage] = '\0'; // on termine la c-string
      indexMessage = 0; // on se remet au début pour la prochaine fois
      messageEnCours = false;
    } else if (indexMessage <= tailleMessageMax - 1) message[indexMessage++] = (char) c; // on stocke le caractère et on passe à la case suivante
    else Serial.println(F("j'ignore!"));
  }
  return messageEnCours;
}

void setup() {
  Serial.begin(115200);
}

void loop() {
  if (! ecouter()) {
    // on a reçu le marqueur de fin
    Serial.print(F("Phrase: [")); Serial.print(message); Serial.println(F("]"));
  }

  // ici on peut faire autre chose

}

et si je tape 123# sur mon clavier, la console dira

[sub][color=blue][nobbc]Octet lu: 0x31 caractere: [1]
Octet lu: 0x32 caractere: [2]
Octet lu: 0x33 caractere: [3]
Octet lu: 0x23 caractere: [#]
Fin de chaine
Phrase: [123][/nobbc]
[/color][/sub]

Voilà avec cela vous avez la base de la gestion asynchrone. la loop() tourne et construit petit à petit votre message au fil de l'eau. et vous êtes prévenu dans la loop() quand le marqueur de fin a été reçu et la c-string construite, ce qui vous permet de déclencher une action, comme par exemple comparer la phrase reçue avec une commande particulière en utilisant les fonctions C standard (stdlib.h et string.h) comme par exemple strcmp() qui va comparer votre message reçu à une commande.

par exemple si vous regardez ce code (nettoyé des impression de debug) si vous tapez Bonjour# dans la console il vous répondra, sinon il ne comprend pas.

const byte tailleMessageMax = 50;
char message[tailleMessageMax + 1]; // +1 car on doit avoir un caractère de fin de chaîne en C, le '\0'

const char marqueurDeFin = '#';

boolean ecouter()
{
  static byte indexMessage = 0; // static pour se souvenir de cette variable entre 2 appels consécutifs. initialisée qu'une seule fois.
  boolean messageEnCours = true;

  while (Serial.available() && messageEnCours) {
    int c = Serial.read();
    if (c != -1) {
      switch (c) {
        case marqueurDeFin:
          message[indexMessage] = '\0'; // on termine la c-string
          indexMessage = 0; // on se remet au début pour la prochaine fois
          messageEnCours = false;
          break;
        default:
          if (indexMessage <= tailleMessageMax - 1) message[indexMessage++] = (char) c; // on stocke le caractère et on passe à la case suivante
          break;
      }
    }
  }
  return messageEnCours;
}

void setup() {
  Serial.begin(115200);
}

void loop() {
  if (! ecouter()) {
    if (!strcmp(message, "Bonjour")) {
      Serial.println(F("Salut, Comment ça va?"));
    } else {
      Serial.println(F("pardon, pouvez vous repeter ?"));
    }
  }
}

ou alors avec une clavier à membrane, si on veut rentrer un mot de passe, par exemple "1221" suivi de # pour valider on pourrait avoir le code suivant

#include <Keypad.h>

const byte ROWS = 4; //4 rangées
const byte COLS = 4; //4 colonnes

char keys[ROWS][COLS] = {   //les symboles, déclarés par leur code ASCII dans le tableau à 2 dimension
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'}
};

byte rowPins[ROWS] = { 34, 36, 38, 40 }; // On connecte ROW0, ROW1, ROW2 and ROW3 à ces pins (je suis sur un MEGA)
byte colPins[COLS] = {41, 39, 37, 35}; // On connecte  COL0, COL1, COL2 and COL3 à ces pins (je suis sur un MEGA)

Keypad membraneKeypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS);  //On initialise une instance de la classe Keypad

const byte tailleMessageMax = 50;
char message[tailleMessageMax + 1]; // +1 car on doit avoir un caractère de fin de chaîne en C, le '\0'

const char marqueurDeFin = '#';
const char motDePasse[] = "1221";

boolean ecouter()
{
  static byte indexMessage = 0; // static pour se souvenir de cette variable entre 2 appels consécutifs. initialisée qu'une seule fois.
  boolean messageEnCours = true;

  char c = membraneKeypad.getKey();
  if (c != NO_KEY) {
    if (c == marqueurDeFin) {
      message[indexMessage] = '\0'; // on termine la c-string
      indexMessage = 0; // on se remet au début pour la prochaine fois
      messageEnCours = false;
    } else if (indexMessage <= tailleMessageMax - 1) message[indexMessage++] = (char) c; // on stocke le caractère et on passe à la case suivante
  }
  return messageEnCours;
}

void setup() {
  Serial.begin(115200);
}
void loop() {
  if (! ecouter()) {
    if (!strcmp(message, motDePasse)) {
      Serial.println(F("Mot de passe correct"));
    } else {
      Serial.println(F("la police est en route"));
    }
  }
  // ici on peut faire autre chose
}

Je vous laisse essayer


Voilà en espérant que cela aide à comprendre et démystifie un peu cette notion d'entrée asynchrone.

Bonne fin de journée !

Jean-Marc.

2 Likes

Merci,sympa les explications

C'est super intéressant ! Merci beaucoup !

Bonjour Jean-Marc,

Un grand merci pour ce tuto utile et très didactique.

Amicalement,
J-F

Bonjour,

Tout d'abord merci de m'instruire comme vous le faites.

J'ai testé votre code et il y a une chose qui ne fonctionne pas pour moi. Après avoir écrit un premier mot (bon ou pas) je peux écrire Bonjour# adéquatement à volonté et la machine ne le reconnaît pas et répond "pardon,vous pouvez répéter?" Lorsque je fais écrire la valeur stocké dans message, la valeur est pourtant bonne.

const byte tailleMessageMax = 50;
char message[tailleMessageMax + 1]; // +1 car on doit avoir un caractère de fin de chaîne en C, le '\0'

const char marqueurDeFin = '#';

boolean ecouter()
{
  static byte indexMessage = 0; // static pour se souvenir de cette variable entre 2 appels consécutifs. initialisée qu'une seule fois.
  boolean messageEnCours = true;

  while (Serial.available() && messageEnCours) {
    int c = Serial.read();
    if (c != -1) {
      switch (c) {
        case marqueurDeFin:
          message[indexMessage] = '\0'; // on termine la c-string
          indexMessage = 0; // on se remet au début pour la prochaine fois
          messageEnCours = false;
          break;
        default:
          if (indexMessage <= tailleMessageMax - 1) message[indexMessage++] = (char) c; // on stocke le caractère et on passe à la case suivante
          break;
      }
    }
  }
  return messageEnCours;
}

void setup() {
  Serial.begin(115200);
}

void loop() {
  if (! ecouter()) {
    if (!strcmp(message, "Bonjour")) {
      Serial.println(F("Salut, Comment ça va?"));
    } else {
      Serial.println(F("pardon, pouvez vous repeter ?"));
    }
  }
}

[/quote]

ça fonctionne parfaitement pour moi...
vous écrivez bien "Bonjour#" avec un B majuscule et le débit de la console est bien à 115200 et vous n'envoyez pas CR et LF ?

(si vous envoyez CR, LF ou les deux - ils restent dans le buffer d'entrée pour le prochain coup et donc la chaîne saisie la seconde fois sera [color=red]\r\n[/color]Bonjour[color=purple]#[/color] (le # n'est pas dans le message, on le dégage) et donc ne correspond pas quand on fait le test avec

strcmp(message, "Bonjour")

Si vous l'imprimez cependant, comme ces caractères ne se voient pas (vous aurez un saut de ligne) vous avez l'impression que c'est OK)

une façon de traiter les caractères non voulus est de tester si le message contient le mot "Bonjour" et au lieu d'utiliser strcmp() on va alors utiliser strstr()

   if (strstr(message, "Bonjour")) {
      Serial.println(F("Salut, Comment ça va?"));
    } else {
      Serial.println(F("pardon, pouvez vous repeter ?"));
    }

une autre façon est de les virer lorsqu'on les reçoit. Vous pouvez aussi mettre tout en majuscule lors de la réception éventuellement si ça n'a pas d'importance dans votre application

par exemple avec un marqueur de fin à '\n' et en ignorant les '\r', et en mettant tout en majuscules on ferait cela

const char marqueurDeFin = '\n';

boolean ecouter()
{
  static byte indexMessage = 0; // static pour se souvenir de cette variable entre 2 appels consécutifs. initialisée qu'une seule fois.
  boolean messageEnCours = true;

  while (Serial.available() && messageEnCours) {
    int c = Serial.read();
    if (c != -1) {
      switch (c) {
        case marqueurDeFin:
          message[indexMessage] = '\0'; // on termine la c-string
          indexMessage = 0; // on se remet au début pour la prochaine fois
          messageEnCours = false;
          break;
        case '\r':
          break; // on l'ignore
        default:
          if (indexMessage <= tailleMessageMax - 1) message[indexMessage++] = toupper((char) c); // on stocke le caractère EN MAJUSCULE  et on passe à la case suivante
          break;
      }
    }
  }
  return messageEnCours;
}

Ahh! Super j'avais bien placer le choix à NL& CR. Merci de m'aider à comprendre tout ça! Vous êtes généreux.

Avec plaisir ! :slight_smile:

1 Like