Gestion de commandes sur port Série

Salut à toutes et tous,

Il arrive que l'on doive discuter avec un appareil connecté sur un port Série (ou de manière plus générale piloté par une sous classe de la classe Stream comme Serial, SoftwareSerial, etc) en lui envoyant des commandes et en attendant une réponse.

Les cas classiques que l'on peut rencontrer

  • un module ESP (langage de commande AT)
  • un module GSM (langage de commande AT)
  • un module GPS (langage de commande parfois spécifique)
  • un appareil de laboratoire (langage de commande spécifique)
  • ...

Le processus est souvent le même cependant:

1/ on envoie une commande en ASCII (commandes AT par exemple) ou en BINAIRE (souvent des appareils industriels) en direction du module (en gros un flux d'octets sur le port de communication).

2/ L'appareil au bout du port de communication reçoit la commande et répond sur ce même port au bout d'un certain temps. La réponse est le plus souvent définie dans un protocole normé, on sait ce que l'on peut recevoir comme réponse à notre commande. Cette réponse est soit en ASCII soit en BINAIRE.

La question fréquente que l'on voit sur les forum c'est comment gérer cette interaction, par exemple j'ai envie d'envoyer un SMS, ou d'en recevoir un, ou de faire une requête GET en langage de commande AT sur un ESP-01...

Je vous propose ici une petite classe [b]StreamDevice[/b] qui permet de gérer cela pour vous. Le code est en PJ dans un ZIP à mettre dans votre répertoire libraries habituel.

Pour faire simple et démontrer l'usage j'ai écrit un petit exemple qui ne requiert aucun autre matériel qu'un Arduino de base avec un seul port série. L'utilisateur simulera la réponse de l'appareil aux commandes.

Le code commence par instancier notre classe:

#include <StreamDevice.h>
StreamDevice fakeDevice(Serial);

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

On dit que fakeDevice discutera avec le module cible sur le port Serial. Bien sûr on n'oubliera pas dans le setup de démarrer le port Série.

Si vous aviez un SoftwareSerial vous pourriez écrire

#include <SoftwareSerial.h>
SoftwareSerial gprsSerial(7,8);

#include <StreamDevice.h>
StreamDevice gprs(gprsSerial);

void setup()
{
  gprsSerial.begin(9600);
  Serial.begin(115200);
  ...

et votre instance pour discuter avec votre module s'appelera gprs et utilisera le port virtuel (instance de SoftwareSerial) gprsSerial sur les pin 7 et 8.

Il y a un second paramètre (par défaut à 20) à ce constructeur qui est la taille du plus grand mot clé que l'on attendra en retour de commande. Si votre appareil répond toujours "OK\r\n" en fin de réponse et pas plus vous pouvez passer en second paramètre 4 ça consommera ansi moins de mémoire.

Une fois notre instance crée, pour lui envoyer des commandes on utilise toutes les fonctions habituelles d'écriture. Vous pouvez faire

fakeDevice.print(...);
fakeDevice.println(...);
fakeDevice.write(...);

avec les options standard comme vous le feriez pour un affichage dans la console (donc support la macro F() ou HEX, BIN pour l'affichage des nombres, ou le nombre de chiffres après la virgule pour un float, ...)

Dans l'exemple, dans le setup() j'envoie une commande imaginaire qui demande à l'utilisateur de répondre par Hello. Pour monter que vous pouvez bâtir votre commande en plusieurs lignes, je le fais en 2 fois.

 fakeDevice.print(F("Say Hello"));            // send commands to your device
  fakeDevice.println(F(" within 5 seconds"));  // you can use print, println, write, etc. all the print usual capabilities

Une fois la commande totale envoyée, il faut le dire à votre instance pour qu'elle sache qu'elle a un résultat à traitrer. cela se fait par la méthode endCommand(). Cette méthode dans sa plus simple expression va attendre un mot clé que vous passez en paramètre. Si vous dites

fakeDevice.endCommand("Hello");

alors vous déclarez que fakeDevice devra attendre le mot clé "Hello" pendant 1 seconde (temps par défaut) maximum. Si vous voulez attendre plus longtemps vous pouvez passer le temps en ms comme second paramètre par exemple pour dire d'attendre 5 secondes on fait:

fakeDevice.endCommand("Hello", 5000ul);

Une fois ces paramètres configurés, le plus souvent on veut attendre la réponse de manière synchrone avant de continuer. La classe fournit donc une méthode awaitKeyword() qui est bloquante au plus pendant le temps indiqué lors du endCommand() (ou si on reçoit le mot clé dans ce cas elle arrête d'attendre bien sûr). Cette méthode retournera soit KEYWORD_OK soit KEYWORD_TIMEOUT en fonction de ce qui a déclenché la fin de réponse.

Cependant parfois il arrive qu'on veuille lancer une commande qui va prendre du temps et donc ne pas tout bloquer en attente de la réponse. Pour cela il existe la méthode answerStatus() qui vous dit où en est le processus d'attente de réponse. Dans ce cas vous aurez soit KEYWORD_PENDING si la fonction est toujours en train d'attendre le mot clé, soit comme auparavant KEYWORD_OK soit KEYWORD_TIMEOUT en fonction de ce qui a déclenché la fin de réponse.

Mais il se peut que vous soyez intéressé par le contenu de la réponse à la commande et donc que vous souhaitiez capturer le message qui se trouve avant le mot clé. Dans ce cas vous pouvez passer en plus 2 paramètres à la méthode endCommand(): un pointeur vers un buffer dont vous avez la responsabilité (il doit être assez grand pour stocker la réponse) et la taille de ce buffer.

Par exemple si vous lancez une commande qui peut prendre 10 secondes et dont la réponse se termine par "OK\r\n" vous pourriez faire:

char tempBuffer[100];
fakeDevice.println(F("une commande importante")); // envoi de la commande
fakeDevice.endCommand("OK\r\n", 10000ul, tempBuffer, 100); // attente 10 secondes de la réponse avec stockage au max de 99 caractères dans tempBuffer

Enfin, pour ceux qui travaillent avec des protocoles en binaire, il existe aussi une version de endCommand() avec une signature adaptée:

   // expecting a random byte stream, optionally capturing the input in a buffer
    bool endCommand(const void* expectedKeyword, const size_t expectedSize, uint32_t maxWait = 1000ul, void* captureBuffer=NULL, size_t captureBufferSize = 0);

Il y a une petite méthode purgeInput() qui présente peu d'intérêt sur des protocoles asynchrones mais si vous voulez nettoyer le buffer et tout ce qui y arrive pendant un certain temps (histoire d'essayer de commencer "propre") vous pouvez appeler

fakeDevice. purgeInput(1000); // vider le buffer et ignorer tout ce qui arrive dans la seconde qui suit

Voilà - je vous laisse explorer le code de test, chargez le et ouvrez la console à 115200 bauds.

StreamDevice.zip (8.96 KB)

Bonsoir

Merci , cela permettra d'ajouter facilement de l'interaction là ou on se contente souvent d'envoyer des commandes sans prendre en compte les réponses, en se bornant à un delay() entre les envois successifs en espérant que la commande précédente ait été bien réceptionnée.

Salut

Oui c’est un peu l’idée. Ça permet facilement de voir si on a reçu le OK par exemple et de ne pas attendre un certain temps juste en priant que ce soit le bon temps à attendre...