Simulateur de capteur, envoie de trame de données sur liaison série

Bonjour à tous,

Je débute sur Arduino et n'y touche pas souvent, j'ai certainement plein de conseil à prendre.
Je souhaite réaliser de la façon la plus simple possible un simulateur de capteur de gaz ( j'ai la doc intégrateur plutôt bien détaillée), ce dernier envoie des données via une liaison série avec une construction de trame bien spécifique :

image

Dans cette trame, certains paramètres resteront fixes, pour d'autres j'aurais besoin de les faire varier mais de façon cyclique (avec un simple delay() probablement )

j'ai un peu de mal a organisé mon code, et je ne voudrais pas perdre du temps bêtement

1 => comment je dois transmettre les données ? ( 1 à 1, par string...? ) pour le moment j'ai construit ma trame avec un tableau

2=> dans ma trame j'ai à la fois des valeurs sur 1 octet et sur 2 octets (dont une que je vais devoir faire varier) comment dois je configurer mon tableau si j'utilise cette solution ? je ne suis pas sur de retrouver mes 21 octets avec ma solution

3 => comment intégrer mes données variables dans ma trame ? dans loop() ou en créant des fonctionspécifiques()

Voila une ébauche, mais je suis loin d'être au bout ( ya certainement des coquilles n'hésitez pas si je suis pas sur le bon chemin) :

// Simulateur capteur multigas CO2  RS232

#include <SoftwareSerial.h>
SoftwareSerial RS232(10, 11);  // RX, TX
unsigned char id = 0; // doit vaier de 0 à 9 en pour identifier trame x/10
byte tamp =0 ; // pour le calcul du chck
  word m1; // variable de type word (2 Bytes)
  word m2; // variable de type word (2 Bytes)
  word m3; // variable de type word (2 Bytes)
  word m4; // variable de type word (2 Bytes)
  word m5; // variable de type word (2 Bytes)
  
  byte sum =0 ; // pour le calcul du check sum uniquement pour la vérification 

void setup() {
  Serial.begin(9600);
  RS232.begin (9600);  //9600 selon doc 
 
} 
void loop() {

++ id %= 10 ; // fait varier id de 0 à 9 en continu 

 
  m1= 00;  // restera fixe
  m2= 489 ; // je vais devoir faire vairier cette valeur (courbe CO2)
  m3 = 0000; // restera fixe
  m4 = 0000; //restera fixe
  m5 = 0000;// restera fixe
 
   unsigned char frame[16];
  frame[0] = 0xaa;              /* Start flag 1 */
  frame[1] = 0x55;              /* Start flag 2 */
  frame[2] = id;              /* Command ID */ // doit varier de 0 à 9         
  frame[3] = 0x01 ; // STS byte 0 no breath 1 Breath restera fixe
  frame[4] = m1 ; // waveform value 5 word 
  frame[5] = m2; //// waveform value 5 word
  frame[6] = m3; // waveform value 5 word
  frame[7] = m4; // waveform value 5 word
  frame[8] = m5; // waveform value 5 word
  frame[9] = 0xB5;  // slow data byte 1
  frame[10] = 0x00; // slow data byte 2
  frame[11] = 0x00; // slow data byte 3
  frame[12] = 0x00; // slow data byte 4
  frame[13] = 0x00; // slow data byte 5 hi byte
  frame[14] = 0xB5; // slow data byte 6  low byte
  

  /*byte de chck */
  for (int i = 2; i < 15; i++) { 
  tamp = tamp + frame[i];

  frame[15] = ~(tamp) + 1; } // inverse de la somme des bytes de la trame pour avoir chksum = 0

  
  /*  vérification du chksum */

  for (int i = 2; i < 16; i++) { /* Skip flag 1 and flag 2. */
    sum += frame[i];   
   // Serial.println(frame[i]); // pour vérifier     
  }    
   /* Checksum OK - use frame */                
  if (!sum) { Serial.println("OKKKKKKK!");// pour vérifier
  for (int i=0; i<16; i++){
  if(i> 16){
  i= 0; break;
   }
  RS232.write(frame[i]); // ecrit la trame de données sur le port série
  Serial.println(frame[i],HEX); // pour vérifier 

  }
  }
      /* Bad checksum - discard frame */
  else { //Serial.println("Pas bien!" ); // pour vérifier 
  
  }
  
}

Merci par avance pour votre aide si précieuse :pray:

est-ce le capteur qui envoie les trames où c'est vous qui devez construire la trame et l'envoyer ?

généralement une approche qui fonctionne quand on a une trame c'est de faire une structure qui est le miroir de la description de la trame (en utilisant __attribute__((packed)) pour forcer l'organisation de la structure).

pour l'envoyer on fait juste un Serial.write(maStructure, sizeof maStructure);

Merci pour ce retour

C'est moi qui dois construire et l'envoyer au meme format que le capteur.
L'idee cest que l'hôte ait l'impression que cest le capteur et qu'il afffiche les valeurs que j'aurai attribué a ma trame.

Si j'avais une fonction simple pour lire les trames du capteur ca m'aiderai aussi, mais je pense pouvoir faire sans.

Tu as besoin d'envoyer les données au format binaire, comme l'indique @J-M-L via la fonction write.
Comme @J-M-L , c'est aussi ce que je ferais d'avoir une structure qui représente ta trame.
du coup tu aurais simplement à définir les différents propriétés
ex :

myData.FLAG1 = 200;
myData.FLAG2 = 100;
...
myData.WaveForm[0] = ... comme je ne connais pas ce que tu veux envoyer exactement, soit tu mettre ton short directement, sois l'element d'un tableau, si c'est ce que tu as.

pour lire la trame c'est pareil. on fait une fonction qui reçoit les octets quand ils arrivent dans un buffer de type uint8_t puis une fois qu'on a reçu le bon compte et que le CHK byte est cohérent vous faites un memcpy() du buffer d'octets dans la structure et vous avez ensuite au travers de la structure accès aux champs

j'ai un petit tuto sur le sujet de la réception asynchrone. Les exemples sont pour du texte mais c'est pareil avec des données binaires

Alors,
la spéc. dit que le trame fait 21 octets.
Tu vois bien que ta trame à toi ne fait que 16 octets.
C'est parce que tasses les 5 words de "Waveform Data" dans 5 octets ( frame[4] à frame[8] ).
un "word" fait 2 octets.
le 1er word de "Waveform Data" occupe les octets 4 et 5
le 2ème word de "Waveform Data" occupe les octets 6 et 7.
etc
ainsi le 1er octet de "Slow Data" occupe frame[14] (et non pas frame[9] comme dans ton code)

Il te faut donc maintenant une méthode pour rentrer m1, m2, ..., m5 à leurs emplacements dans la trame.
A ce stade, IL MANQUE UNE INFO CRUCIALE.
Ces words qui font 2 octets ont un octet de poids fort et un octet de poids faible.
Par exemple pour m2 qui vaut 489, il s'écrit en héxa 0x1E9, donc l'octet de poids fort contient 1 et l'octet de poids faible contient 0xE9 (=233 en décimal)
Est-ce que les words qui sont dans la trame sont représentés avec leur octet de poids fort en premier (format dit big endian, ou gros-boutien) ou bien avec l'octet de poids faible en 1er (little-endian ou petit-boutien) ?
Dans le 1er cas, il faut écrire

frame[6] = 1;
frame[7] = 0xE9;

alors que dans le second cas c'est :

frame[6] = 0xE9;
frame[7] = 0x1;

Cette information ( little ou big endian ?) est indispensable !
REmarque : si tu ne l'as pas, essaye l'une puis l'autre...

De façon générale, en big-endian:

frame[4] = (char] m1 >> 8;
frame[5] = (char) m1 & 0xFF;

et en little endian c'est l'inverse

frame[4] = (char) m1 & 0xFF;
frame[5] = (char] m1 >> 8;

Par ailleurs, quand tu calcules la somme des octets de la trame, tu oublies de mettre tamp à zéro avant de commencer.

Et puis dans loop() il manque un delay(). En effet je ne pense pas que tu veuilles envoyer la trame "à fond la caisse", sans temporisation. L'ajout d'un delay() à la fin de loop() fera l'affaire.

C'est faire fi des problème d'endianness (voir mon post ci-avant).
Ca ne marche que si l'endianness des 2 machines (émettrice et réceptrice) sont les mêmes.

Non, cela laisse problème de boutisme au même niveau, il faut toujours respecter l'ordre du boutisme lorsque tu affecte les valeurs sur plus d'un octet.
Puisque la structure est vu comme un buffer d'octet du point de vue de la fonction qui envois les données.

je n'en fais pas fi, il fait bien sûr en tenir compte dans les waveform data

la doc à l'air de dire

Integer data is transmitted as signed two-complement values. MSB transmitted first.

➜ donc on sait qu'ils sont en big endian. à prendre en compte quand on les fabrique et ensuite c'est une affectation ou alors on fait l'affectation avec inversion des octets si les waveform data sont construites sur l'arduino qui sera little endian

Je répète doucement :
mettre une trame contenant des datas de plus d'un octet dans une structure, et envoyer la structure en un bloc - et la recevoir dans une struct à l'autre bout,
ça ne marche que si l'emmetteur et le récepteur ont des processeurs de même endianness (de même boutisme)
Dans le cas contraire, ça ne fonctionne pas.

Désolé, j'avais pas vu ça. Mais ça ne change rien à ce que j'ai dit.

ce n'est pas tout à fait cela. si j'envoie 10 octets dans un certain ordre, j'ai ces 10 octets qui arrivent dans le même ordre de l'autre côté.

Si je sais que pour les données tenant sur plus d'un octet le récepteur souhaite les voir arriver MSB first, ben je dois en tenir compte lorsque je fabrique les données.

rien ne m'empêche d'avoir pour
image

struct __attribute__((packed)) t_frame {
  uint8_t flag1;
  uint8_t flag2;
  uint8_t id;
  uint8_t sts;
  uint16_t waweFormData[5]; // attention BIG ENDIAN
  uint8_t slowData[6];
  uint8_t chk;
};

t_frame uneFrame;
...
Serial.write(uneFrame, sizeof uneFrame);

mais bien sûr lorsque je mets quelque chose dans uneFrame. waweFormData[0] il faut que je tienne compte du format attendu, donc big endian si c'est une donnée calculée en local dans un uint16_t en little endian ➜ il faudra inverser les octets lors de l'écriture. On peut utiliser highByte() et lowByte() pour cela.

  uint16_t little = 0xABCD;                                 // si on a cette valeur en little endian
  uint16_t big = 256u * lowByte(little) + highByte(little); // alors on veut la stocker sous cette forme pour transmission 

Note en passant: en C++ on ne reçoit pas dans une structure, on reçoit dans un buffer d'octet et on fait un memcpy() dans la structure.

J-M-L tu charries, évidemment que si on fait comme tu dis ça marche. Mais on se demande un peu où est l'intérêt d'utiliser une structure dans ce cas. Surtout pour un débutant (si tu oublies packed c'est mort, si tu mets int à la place de int16_t ça peut tout casser...). Oui, le seul intéret est de ne pas avoir à compter les octets dans la trame, c'est déjà ça.

Et puis on peut très bien recevoir une trame directement dans une structure (sans memcpy() ) en passant l'adresse de la struct à la fonction read().

je ne sais pas si je charie, l'idée c'est quand même de se simplifier la vie pour avoir une seule structure à transmettre. On peut utiliser des champs de bits et des types plus complexes tant qu'on respecte le "boutisme".
Je trouve que c'est plus lisible. Mais bien sûr un tableau d'octets peut faire l'affaire aussi.

Non pas en C++ si vous voulez respecter la norme.

ça fonctionne avec GCC actuellement mais c'est Undefined Behavior dans le standard qui dit bien

When it is needed to interpret the bytes of an object as a value of a different type, std::memcpy or std::bit_cast (since C++20)can be used

merci à tous pour vos réponse, je viens de rentrer et je pense qu'il va me faloir un peu de temps pour digérer tout ca.

Mais grâce à vous je commence à comprendre comment je vais devoir structurer tt ca.

Voila un peu plus d'info pour répondre aux question sur big endian ou gros- boutien avec un exemple de trame émise par le capteur que je cherche à simuler :
image

a priori ils utilisent aussi ce système pour atribuer 2 octects/6 pour certain id dans slow data pour renvoyer par exemple le SN du capteur qui ne tient pas dans sur 1 octet :
image

image
image

a priori je vais devoir gérer hi/low byte en dehors de waveform gas ca complique un peu le truc

Je vais tenter de mettre ca au propre, mais pour débuter ce n'est pas ce qu'il ya de plus simple.

rien de bien compliqué dans cet exemple

soit vous faites un tableau d'octets comme proposé par @biggil et vous les mettez dans l'ordre

uint8_t uneTrame[21] = {
  0xAA,                                 // FLAG1
  0x55,                                 // FLAG2
  0x02,                                 // ID
  0x02,                                 // STS
  0x01, 0xEA,                           // WaveForm 0
  0x00, 0x00,                           // WaveForm 1
  0x00, 0x00,                           // WaveForm 2
  0x00, 0x00,                           // WaveForm 3
  0x00, 0x00,                           // WaveForm 4
  0xFF, 0x26, 0x00, 0x00, 0x03, 0xB6,   // Slow Data
  0x33                                  // CHK
};

...

serial.write(uneTrame, sizeof uneTrame);

soit avec la structure

struct __attribute__((packed)) t_frame {
  uint8_t flag1;
  uint8_t flag2;
  uint8_t id;
  uint8_t sts;
  uint16_t waweFormData[5]; // attention BIG ENDIAN
  uint8_t slowData[6];
  uint8_t chk;
};

t_frame uneFrame = {
  0xAA,                                         // FLAG1
  0x55,                                         // FLAG2
  0x02,                                         // ID
  0x02,                                         // STS
  {0xEA01, 0x0000,  0x0000,  0x0000,  0x0000},  // WaveForm
  {0xFF, 0x26, 0x00, 0x00, 0x03, 0xB6},         // Slow Data
  0x33                                          // CHK
};
...
Serial.write(uneFrame, sizeof uneFrame);

l'avantage de la structure pour moi c'est qu'on peut être assez lisible

if (uneFrame.sts = 0b11) { ...}

plutôt que

if (uneFrame[3] = 0b11) { ...}

bien sûr on peut faire un enum avec des mots clés qui permette de faire

enum : byte {FLAG1, FLAG2, ID, STS, /* ici plus merdique pour le tableau */, ..., CHK};
if (uneFrame[STS] = 0b11) { ...}

mais je trouve cela plus lourd

Merci pour tes retours et explications, j'ai fait les petites modifs, mais j'ai l'impression que ca a planté la verif du chksum, pourtant j'ai bien le dernier Byte à 0 :face_with_raised_eyebrow:
voici le code : `

#include <SoftwareSerial.h>
SoftwareSerial RS232(10, 11);  // RX, TX
unsigned char id = 0; // doit vaier de 0 à 9 en pour identifier trame x/10
 // pour le calcul du chck
  word m1; // variable de type word (2 Bytes)
  word m2; // variable de type word (2 Bytes)
  word m3; // variable de type word (2 Bytes)
  word m4; // variable de type word (2 Bytes)
  word m5; // variable de type word (2 Bytes)
  
   

void setup() {
  Serial.begin(9600);
  RS232.begin (9600);  //9600 selon doc 
 
} 
void loop() {
byte tamp =0 ;// remetre tamp à 0
byte sum = 0 ; // pour le calcul du check sum uniquement pour la vérification
++ id %= 3 ; // fait varier id de 0 à 9 en continu 

 
  m1= 0000;  // restera fixe
  m2= 489 ; // je vais devoir faire vairier cette valeur (courbe CO2)
  m3 = 0000; // restera fixe
  m4 = 0000; //restera fixe
  m5 = 0000;// restera fixe
 
   unsigned char frame[20];
  frame[0] = 0xaa;              /* Start flag 1 */
  frame[1] = 0x55;              /* Start flag 2 */
  frame[2] = id;              /* Command ID */ // doit varier de 0 à 3        
  frame[3] = 0x01 ; // STS byte 0 no breath 1 Breath restera fixe
  frame[4] = m1 >> 8; // waveform value BIG BYTE
  frame[5] = (char) m1 & 0xFF;// waveform value 5 LOW BYTE
  frame[6] = (char) m2 >> 8; // waveform value BIG BYTE
  frame[7] = (char) m2 & 0xFF; // waveform value 5 LOW BYTE
  frame[8] = (char) m3 >> 8; // waveform value BIG BYTE
  frame[9] = (char) m3 & 0xFF; // waveform value 5 LOW BYTE
  frame[10] = (char) m4 >> 8; // waveform value BIG BYTE
  frame[11] = (char) m4 & 0xFF; // waveform value 5 LOW BYTE
  frame[12] = (char) m5 >> 8; // waveform value BIG BYTE
  frame[13] = (char) m5 & 0xFF; // waveform value 5 LOW BYTE
  frame[14] = 0xB5;  // slow data byte 1
  frame[15] = 0x00; // slow data byte 2
  frame[16] = 0x00; // slow data byte 3
  frame[17] = 0x00; // slow data byte 4
  frame[18] = 0x00; // slow data byte 5 
  frame[19] = 0xB5; // slow data byte 6 
  

  /*byte de chck */
  for (int i = 2; i < 20; i++) { 
  tamp = tamp + frame[i];

  frame[20] = ~(tamp) + 1; } // inverse de la somme des bytes de la trame pour avoir chksum = 0

  
  /*  vérification du chksum */

  for (int i = 2; i < 21; i++) { /* Skip flag 1 and flag 2. */
    sum += frame[i];   
    Serial.println(frame[i],HEX); // pour vérifier     
  }    
   /* Checksum OK - use frame */                
  if (!sum) { Serial.println("OKKKKKKK!");// pour vérifier
  for (int i=0; i<21; i++){
  if(i> 20){
  i= 0; break;
   }
  //RS232.write(frame[i]); // ecrit la trame de données sur le port série
  //Serial.println(frame[i],HEX); // pour vérifier 

  }
  }
      /* Bad checksum - discard frame */
  else { Serial.println("Pas bien!" ); // pour vérifier 
  
  }
  delay(500);
}`

avec cette méthode, si je fait varier mes valeurs dans une autre fonction, ( type ID ou char m2 pour les waveform ) je ne risque pas de tout planter ? comme le calcul du chk ?

détail, tu proposais

frame[4] = (char] m1 >> 8;
frame[5] = (char) m1 & 0xFF;

j'ai du faire ca() pour que capasse (comme je ne suis pas un pro de la syntaxe je veux pas faire de boulette) :

 frame[5] = (char) m1 & 0xFF;
  frame[6] = (char) m2 >> 8;

en fait id va varier de 0 à 2 (0, 1, 2, 0 , 1, 2, ...)


votre frame contient des unsigned char

pourquoi y coller des char ?

pour simplifier la lecture on peut utiliser highByte() et lowByte() comme dit dans un post précédent


cet adresse n'existe pas dans le tableau

car vous l'avez déclaré avec 20 cases, pas 21.


les 2 premiers octets ne sont pas dans le checksum?
pourquoi mettre à jour frame[20] en permanence dans la boucle ? attendez d'avoir fini le calcul

y'a d'autres trucs mais commencez déjà par mettre un peu d'ordre là dessus

C'est ce que je voulais, les autres Id je vais essayer de m en passer, mais j ai pas/mal changer le comm

J'ai bêtement repris le code sans me posé la question du conflit.

Ca je vais bosser dessus demain matin :grin:

Pourtant c'etait gros ! :sweat_smile:

Excusez pes questions de noob, mais

Du coup, je fais une fonction dedier et j'appel le résultat ?

Non, juste faire l’affectation (si c’est la bonne formule) une fois la boucle for terminée

tamp = 0;
for (byte i = 2; i < 20; i++) tamp +=  frame[i]; // on additionne modulo 256 tous les octets
frame[20] = ~tamp + 1; // une fois fait, on affecte le checksum