Les modules de communication série Yoctopuce sont plus que des simples interfaces: ils sont capables d'interroger et d'analyser de manière autonome les données provenant d'un appareil quelconque pour ensuite présenter les résultats à la manière d'un capteur Yoctopuce et/ou les enregistrer dans le datalogger intégré. Mais pour cela, il faut indiquer au module Yoctopuce comment gérer le dialogue avec l'appareil. Voici donc quelques exemples en complément à la documentation pour vous faciliter la tâche.
Pour les scénarios les plus standard, le wizard de création de job du Yocto-RS232, Yocto-Serial, Yocto-RS485 et Yocto-SPI vous propose directement une interface dédiée. Par exemple, le Yocto-RS232 propose l'interface suivante pour configurer le décodage de messages NMEA:
Interface simplifiée pour configurer le décodage de messages
Les cas qui vont nous intéresser ci-dessous sont justement ceux qui ne sont pas couverts par les interfaces dédiées, et pour lesquels il va falloir spécifier le format des messages.
Décodage des messages envoyés par l'appareil
Pour commencer, nous allons voir comment apprendre au module Yoctopuce à décoder un message quelconque. Une manière assez instructive de le faire est de regarder comment les scénarios standards évoqués ci-dessus fonctionnent. Pour cela, il suffit de définir une tâche en utilisant l'interface simplifiée, puis de la réouvrir et de choisir l'option Use a custom protocol. Cela fait apparaître les commandes qui auraient pu être utilisées pour définir manuellement la tâche.
Par exemple si vous définissez un tâche qui analyse deux valeurs séparées par des virgules (format CSV), vous obtiendrez le protocole suivant:
La commande expect sert à reconnaître et à décoder les messages reçus qui correspondent à un format spécifié. Les éléments qui nous intéressent particulièrement sont les groupes entre parenthèses. Ils correspondent aux valeurs qui doivent être lues et affectées aux genericSensors. L'indication $1, $2, ... indique à quel genericSensor la valeur doit être affectée, et l'indication FLOAT indique le type de valeur attendue, ce qui influe simultanément sur les caractères acceptés pour l'expression et le décodage qui en est fait.
La commande en entier s'interprète donc comme suit:
- ($1:FLOAT): lire un nombre à virgule flottante, et l'affecter au genericSensor1
- [,]: attendre une virgule (les parenthèses carrées sont ici en réalité superflues)
- ($2:FLOAT): lire un nombre à virgule flottante, et l'affecter au genericSensor2
- .*: ignorer tout le reste de la ligne
Prenons un exemple un peu plus compliqué, par exemple le décodage d'un message NMEA donné en exemple dans l'illustration ci-dessus. On voit que la commande suivante est utilisée:
Pour mieux comprendre la chaîne définissant le format, voici un exemple de message NMEA qui est reconnu par cette chaîne:
$GPGGA,101558,3852.1553,N,07703.2147,W,1,14,1.5,345.6,M,46.9,M,,*47
Il s'interprète comme suit:
- $GPGGA: identifiant d'un message indiquant une position
- 101558: heure du message (10:15:58 UTC)
- 3852.1553,N: latitude, en degrés et minutes
- 07703.2147,W: longitude, en degrés et minutes
- 1: méthode de détermination (1=GPS, 2=DGPS, 3=PPS)
- 14: nombre de satellites suivis
- 1.5: dilution horizontale de la mesure
- 345.6,M: altitude en mètres au dessus de la mer
- ...: le reste du message ne nous intéresse pas
Si l'on reprend la chaîne de décodage, on voit qu'elle fonctionne selon le principe de la correspondance par fragments (pattern matching): tant que les fragments de texte littéraux correspondent, le message est accepté. Les expressions avec parenthèses carrées permettent de définir un ensemble de caractères autorisés, et l'étoile dénote la répétition à volonté, comme dans les expressions régulières classiques.
Cette chaîne de décodage utilise d'autres type de valeur que FLOAT. Voici donc pour référence la liste exhaustive des types de décodages supportés à ce jour, que vous retrouverez aussi dans la documentation:
- ($x:INT) permet de reconnaître une valeur entière (en base 10) qui sera affectée à la fonction genericSensorX. Par exemple, {$3:INT} permet de reconnaître un nombre entier et l'affecter à la fonction genericSensor3
- ($x:FLOAT) permet de reconnaître une valeur décimale (nombre à virgule), qui sera affectée à la fonction genericSensorX. La notation scientifique (par ex. 1.25e-1) est reconnue.
- ($x:DDM) permet de reconnaître une valeur décimale en degrés-minutes-décimales telle qu'utilisée dans le standard NMEA.
- ($x:BYTE) permet de reconnaître une valeur entière entre 0 et 255 codée en en hexadécimal (comme c'est le cas pour les protocoles en mode binaire). Si la valeur est dans la plage -128...127, on utilisera ($x:SBYTE) à la place (signed byte). La valeur décodée sera affectée à la fonction genericSensorX
- ($x:WORD) ou ($x:SWORD) permet de la même manière de décoder une valeur en hexadécimal sur 16 bits, respectivement non signée ou signée, qui sera affectée à la fonction genericSensorX. On suppose alors que les octets sont dans l'ordre d'écriture usuel, soit l'octet de poid fort en premier (big-endian), comme par exemple 0104 pour représenter la valeur 260.
- ($x:WORDL) ou ($x:SWORDL) ont le même effet que les deux précédentes, mais supposent que les octets sont d'en l'ordre little-endian, c'est-à-dire l'octet de poids faible en premier (par exemple 0401 pour représenter la valeur 260).
- ($x:DWORD) ou ($x:SDWORD) permettent de la même manière de décoder un nombre sur 32 bit en big-endian (non-signé ou signé).
- ($x:DWORDL) ou ($x:SDWORDL) permettent de la même manière de décoder un nombre sur 32 bit en little-endian (non-signé ou signé).
- ($x:DWORDX) ou ($x:SDWORDX) permettent de la même manière de décoder un nombre sur 32 bit en mixed-endian, soit deux mots de 16 bits chacun représenté en big-endian, mais le mot de poids faible en premier et celui de poids fort ensuite.
- ($x:HEX) permet de reconnaître une valeur en hexadécimal de longueur indéfinie (1 à 4 octets), qui sera affectée à la fonction genericSensorX.
- ($x:FLOAT16B) et ($x:FLOAT16L) permettent de décoder un nombre flottant codé en hexadécimal selon le standard IEEE 754 sur 16 bits, respectivement avec les octets ordonnés en big-endian ou little-endian
- ($x:FLOAT16D) permet de décoder un nombre flottant codé en hexadécimal sur deux octets, avec le premier octet qui contient la mantisse et le deuxième octet qui contient l'exposant décimal signé.
- ($x:FLOAT32B) et ($x:FLOAT32L) permettent de décoder un nombre flottant codé en hexadécimal selon le standard IEEE 754 sur 32 bits, respectivement avec les octets ordonnés en big-endian ou little-endian
- ($x:FLOAT32X) permet de décoder un nombre flottant codé en hexadécimal selon le standard IEEE 754 sur 32 bits, avec les octets ordonnés en mixed-endian, soit deux mots de 16 bits chacun représenté en big-endian, mais le mot de poids faible en premier et celui de poids fort ensuite.
La représentation des nombres flottants étant limitée à 3 décimales dans les modules Yoctopuce, il est possible de convertir l'ordre de grandeur des nombres flottants lus par les expressions FLOAT, FLOAT16 et FLOAT32 en les préfixant d'un M pour retourner des millièmes, un U pour les millionièmes (U comme micro) et d'un N pour les milliardièmes (N comme nano). Ainsi, si l'on reconnait la valeur 1.3e-6 avec l'expression ($1:UFLOAT), la valeur affectée au genericSensor3 sera 1.3.
Interactions automatiques
Souvent, il ne suffit pas d'attendre que des messages soient envoyés par l'appareil, mais il faut les solliciter. On peut donc rajouter des commandes qui envoient des messages sur le port série. Par exemple, on pourrait définit la tache suivante suivante pour établir une communication via un modem série:
expect "OK"
writeLine "ATDT0123456789"
expect "CONNECT"
writeLine "Hello world"
wait 1000
write "+++"
wait 1000
writeLine "ATH"
expect "OK"
Cette tâche envoie la commande AT, attend une réponse OK. Si la réponse OK ne vient pas, la tâche recommencera plus tard depuis le début. Sinon, la commande suivante ATDT0123456789 est envoyée, et la tâche attend la réponse CONNECT, etc. Notez qu'il est possible de rajouter des temporisations entre les envois à l'aide de la commande wait, avec un délai indiqué en millisecondes.
Autre exemple: essayons de voir maintenant comment fonctionne la lecture d'un registre MODBUS. C'est typiquement une tâche qui requiert une interaction, car les appareils MODBUS n'envoient pas spontanément leurs mesures mais attendent des ordres de lecture. Si l'on crée par exemple une tâche qui lit le registre 40008 et qu'on la réouvre comme un custom protocol, on obtient les commandes suivantes:
expect ":010302($1:WORD).*"
En cherchant sur internet le document MODBUS Application Protocol Specification, vous trouverez la syntaxe des messages MODBUS qui vous permettra de comprendre ce protocole. Le format de la commande MODBUS envoyée est le suivant:
- 01: message addressé au périphérique 01
- 03: commande "Read MODBUS register"
- 00007: numéro du registre (0 pour 40001, 1 pour 40002, etc.)
- 00001: nombre de registres à lire
Le format de la réponse attendue est le suivant:
- :01: réponse du périphérique MODBUS 01
- 03: réponse à une commande "Read MODBUS register"
- 02: nombres d'octets dans la réponse (2 octets par registre)
- xxxx: valeur du registre (sur 2 bytes)
- ...: un code de détection d'erreur, que nous pouvons ignorer
Notez que comme il ne s'agit pas d'un protocole textuel mais binaire, les messages reçus sont automatiquement convertis en hexadécimal avant d'être interprétés par la commande expect, pour faciliter leur traitement.
On voit maintenant facilement comment cette tâche pourrait être modifiée pour lire deux registres à la fois et les interpréter comme un nombre flottant sur 32 bits par exemple: il suffit de changer le nombre de registres à lire dans la commande writeMODBUS, et de changer la longueur de la réponse attendue et son décodage:
expect ":010304($1:FLOAT32).*"
On peut naturellement aussi lire plusieurs registres disjoints dans la même tâche en ajoutant d'autres commandes writeMODBUS et expect.
Dialogues complexes
Dans certains cas assez rares, le format de la réponse de l'appareil n'est pas toujours le même, et on ne peut donc pas se contenter d'un simple expect pour l'analyser: il en faut plusieurs différents qui fonctionnent en parallèle. Cela peut être le cas par exemple quand un appareil peut répondre réessayer ou donner la réponse attendue.
En général, la meilleur façon de régler le problème est de créer plusieurs tâches différentes:
- Une tâche périodique qui envoie la commande initiale
- Une tâche réactive gère la réponse réessayer
- Une deuxième tâche réactive qui gère la réponse contenant la mesure
Cela fonctionne car les tâches réactives peuvent se déclencher à tout moment, dès que la commande expect par laquelle elles commencent est satisfaite par un message reçu.
Par contre, pour vous assurer de ne pas interférer entres elles, les tâches périodiques ne fonctionnent pas en parallèle: quelle que soit la période configurée, une tâche périodique ne démarre jamais avant le fin de la tâche périodique précédente.
Conclusion
Avec ces explications, vous devriez être paré pour écrire votre propre système de gestion de protocole personnalisé. Cela peut demander quelques essais, mais en observant le dialogue dans la fenêtre de communication ou via la petite application fournie dans un article précédent, vous devriez pouvoir vous en sortir. En bien sûr, le support Yoctopuce est toujours là pour vous aider en cas de difficulté.