LORA - transmission de donnée structurées entre Arduino équipés de LORA-RFM90

Ce billet s'attarde sur un problème d'ordre technique... et se développera certainement sur plusieurs articles.
Le tutoriel du Feather Lora RFM95 (en cours de traduction) indique comment envoyer et réceptionner des chaînes de caractères... Cool!
Cependant, dans la vie réelle, ce sont des données qu'il faut envoyer...

Question
Comment transmettre efficacement les données de senseurs entre deux Feather LORA RFM95 ?
Feather 32u4 (compatible Arduino) avec module LORA RFM95

Le but étant de surveiller la boîte aux lettres ainsi que la température et humidité extérieur.

Deux options
L'une est d'envoyer les données sous forme de chaine de caractère, l'autre d'envoyer les données brute d'une structure.

Options 1: Les chaînes de caractères (PAS BIEN!)
La réponse la plus évidente qui germe chez la plupart d'entre nous c'est "on va transformer les valeurs en chaîne de caractère et transmettre le tout".

Harf! les chaînes de caractères sur Arduino c'est PASSSSS BIEEENNNNNN!
Nous n'avons que 2Ko de RAM... par conséquent, s'il y a beaucoup de données à envoyer, il faudra morceler votre émissions en plusieurs chaînes de caractères et faire l'émission au compte goûte.
Ce n'est pas le pire...

Côté réception, vous manquez aussi de mémoire... faire du parsing de chaîne de caractère est:
  1. Coûteux en temps machine
  2. Coûteux en mémoire
  3. Error prone (le risque de laisser traîner un bug est plus que certain).
J'ai garder le meilleur pour la fin... vous allez devoir développer une couche de "type protocole" pouvant détecter les erreurs. Bien oui... si vous envoyez en morceau... il faut pouvoir réceptionner en morceau (voire une partie puis plus rien... a cause d'un crash).
 
Options 2: transmettre une structure binaire (COOL)
Cette option n'est possible que si nous utilisons 2 arduino (Feather) avec le même microcontrôleur. Dans notre cas de figure, il s'agit de Feather avec un microcontrôleur 32u4.

Dans ce cas, les float, int, byte, char, ... sont encodé de la même façon dans la mémoire.
Mieux encore, un struct (l'équivalent d'un "record") est également enregistré de la même façon... et de façon continue.

typedef struct sensorData_t{
  byte data_version;   // Version number of this structure 
  int  counter;        // Increasing counter (at each sensor read) 
  float rel_humidity;
  float temperature;
  byte  mail_counter;
  float bat_volts;
};

Donc, si on arrive a copier et envoyer tous les octets (bytes) utilisés par cette structure, nous pourrions arriver à les restaurer dans un autre Arduino en les recopiant dans l'espace mémoire du même struct (comprenez avec une définition exactement identique)

Viva union struct... grâce à lui, nous allons pouvoir voir la structure sensorData_t comme la structure qu'elle est .... ou comme un tableau d'octet (de bytes) ayant la même taille mémoire que la structure sensorData_t.

typedef union packetData_t{
  sensorData_t data;
  byte data_buffer[sizeof(sensorData_t)];
};

Utiliser union struct - transmission de données comme un Buffer d'octets
Du coup, il devient possible de transmettre les données comme une série d'octet (autrement dit "un buffer")... pratique pour le RFM95, connexion série, bus I2C.

Voici un programme d'exemple qui démontre cette fonctionnalité:
  1. Il créer une structure packetData_t et la rempli la structure en appelant la fonction read_sensors() .
  2. Il affiche le contenu de packetData_t en accédant au différents octets de la mémoire qui le compose (grâce à la définition byte data_buffer[sizeof(sensorData_t)] de l'union struct) 
  3. Ceci fait, nous créons un nouveau buffer (espace mémoire) de 25 octets... il agira comme l'équivalent du buffer de réception utilisé lors d'une communication I2C, Série ou Lora RFM95.
    Comme si nous avions transmis l'information pour de vrai :-)
    Voyez la ligne "--- Simulate the reception of data via a buffer ---"
  4. Nous affichons les données "transmises"
  5. Nous utilisons decode_sensor_data() qui recharge le buffer de réception dans une structure packetData_t (viva la définition byte data_buffer[sizeof(sensorData_t)] de l'union struct)... PUIS nous accédons aux différent champs de la définition sensorData_t pour afficher les valeurs... c'est trop de la balle!!
void loop() {
  // Emetteur Software
  read_sensors( &info.data );
  
  // Print the Buffer of data
  Serial.print( "data_buffer : " );
  for (int k=0; k < sizeof(sensorData_t); k++){
     Serial.print( info.data_buffer[k], HEX );
     Serial.print( ' ' );
  };
  Serial.println(" ");
  
  // --- Simulate the reception of data via a buffer ---
  // Just copy the data to a new buffer at new memory location
  // (totally separate the emission <-> reception code in 
  // this sketch)
  
  // declare a new buffer (prefer to declare as global for a realapplication)
  Serial.println( "Transmission..." );
  char buff[25];     
  int buffSize;
  for( int r=0; r < sizeof( info.data_buffer ); r++ ){
     buff[r] = info.data_buffer[r];
  } 
  buffSize = sizeof( info.data_buffer );
 
  //--- Decode the receipt Buffer ---
  decode_sensor_data( buff, buffSize );
  
  // Wait 5 sec to repeat
  Serial.println( "--- END OF TREATMENT ---" );
  delay( 5000 );
}

Résultat du programme dans le moniteur série d'Arduino

temp : 29.3999996185
%hum : 81.3000030517
data_buffer : 1 B8 4 9A 99 A2 42 33 33 EB 41 4 66 66 66 40  
Transmission...
Receipt data: 1 B8 4 9A 99 A2 42 33 33 EB 41 4 66 66 66 40  
Version     : 1
Counter     : 1208
Temp.       : 29.3999996185
%Humidity   : 81.3000030517
Mail Counter: 4
Bat Volts   : 3.5999999046
--- END OF TREATMENT ---
temp : 17.7000007629
%hum : 71.0999984741
data_buffer : 1 B9 4 33 33 8E 42 9A 99 8D 41 4 66 66 66 40  
Transmission...
Receipt data: 1 B9 4 33 33 8E 42 9A 99 8D 41 4 66 66 66 40  
Version     : 1
Counter     : 1209
Temp.       : 17.7000007629
%Humidity   : 71.0999984741
Mail Counter: 4
Bat Volts   : 3.5999999046
--- END OF TREATMENT ---

Inspectons read_sensors()
Prenons le temps de nous pencher sur  read_sensors( &info.data ) ... nous lui passons l'adresse de la structure à remplir. Oui, c'est une histoire de pointeur... faut aussi économiser la mémoire.

packetData_t info;

void read_sensors(struct sensorData_t *data ) {
   // Création d'une valeur de température aléatoire 
   // entre 0 et 30.0 degrés
   int r = random( 300 );
   float temp = ((float)r)/10; 
   Serial.print( "temp : ");
   Serial.println( temp, DEC );
   // Humidité relative %rel aléatoire entre %45 et 90% 
   r = random(450);
   float hum = ((float)r)/10 + 45;
   Serial.print( "%hum : ");
   Serial.println( hum, DEC );
   
   // Stockage des données des senseurs dans la structure
   counter += 1; 
   data->data_version = 1;
   data->counter = counter;
   data->rel_humidity = hum;
   
   data->temperature = temp;
   data->mail_counter = 4;
   data->bat_volts = 3.6; 
}

Après l'appel read_sensors( &info.data ) dans le boucle principale loop(), info.data contient les différentes valeurs à transmettre.

Inspectons le code de decode_sensor_data()
La fonction est définie comme suit:
void decode_sensor_data( char *aBuffer, int iBufferSize )

Nous lui passons donc le buffer de réception et la longueur de celui-ci.

void decode_sensor_data( char *aBuffer, int iBufferSize ){ 
   // Declaration d'une structure pour stocker les données
   packetData_t packet; 
  
  // Transférer les octets du buffer dans l'union struct 
  // c'est une copie octet par octet du contenu mémoire 
  // donc on passe le data_buffer de l'union struct
  for( int k=0; k<iBufferSize; k++ ){
      // On évite de dépasser la taille mémoire disponible
      if( k < sizeof( sensorData_t )) {
          packet.data_buffer[k] = aBuffer[k];
      }
  } 
  
  // Affichage des données copiées.
  // Nous passons par le "data" de l'union struct pour avoir
  // accès aux différentes valeurs que nous venons de transmettre
  Serial.print( "Version     : "); 
  Serial.println( packet.data.data_version );
  Serial.print( "Counter     : ");
  Serial.println( packet.data.counter );
  Serial.print( "Temp.       : "); 
  Serial.println( packet.data.temperature, DEC );
  Serial.print( "%Humidity   : "); 
  Serial.println( packet.data.rel_humidity, DEC );
  Serial.print( "Mail Counter: "); 
  Serial.println( packet.data.mail_counter );
  Serial.print( "Bat Volts   : ");
  Serial.println( packet.data.bat_volts, DEC );  
}  

Le code
Le code complet de cet exemple est disponible ici

La transmission du Buffer entre Feathers
C'est bien beau tout cela... mais l'exemple Feather transmet des chaînes de caractères... pas un transmission d'un buffer!

Sur le fond, transmettre une string... c'est transmettre un buffer dont le dernier caractère est un NUL #0 à la fin. Il doit donc être possible de transférer un buffer (via une fonction un peu moins user friendly).

Ce sera l'objet d'un autre article... dans le pire des cas (mais vraiment le pire!), nous ferons une transmissions de la représentation hexadécimale... ce qui nécessitera une chaîne de caractère de ( sizeof(sensorData_t)*2 )+1 .
Le +1 c'est parce qu'il faut un NUL à la fin de la chaîne de caractère.

Aucun commentaire