Comment extraire une partie d'une trame reçue par Serial?

Bonjour à toutes et tous.
J'ai un poste de radioamateur, un YAESU FTDX-3000. Il dispose d'une interface série RS-232 pour dialoguer par des commandes CAT, pour Computer Aided transciever (je crois), à la manière assez simple des commandes AT pour les puces GSM et GPS entre autres.
Et c'est ce qui m'amène ici.

Voici le PDF officiel qui détaille tout : PDF CAT YAESU

Par exemple on envoie "fa;" et on reçoit la fréquence en cours d'utilisation, pour la fréquence 14.250 MHz on recevra "FA14250000;".

J'ai essayé simplement avec un un terminal série et un câble FTDI suivi d'un convertisseur de niveaux MAX3232, ça fonctionne.
Je peux envoyer et recevoir.

J'ai ensuite essayé avec un Arduino Pro Micro et je suis partit de l'exemple Multiserial, ça fonctionne aussi. C'est à dire que quand j'envoie "fa;" je reçois bien dans le terminal série FA12123123; par exemple.

Oui mais voilà, je souhaite extraire la partie 12123123 pour la réexploiter, dès le début du programme, car je souhaite modifier cette fréquence avec un encodeur rotatif. Me faire un bouton déporté, en gros.
J'ai tenté pas mal de choses avec des Sprintf et des boucles FOR pour récupérer les caractères un à un dans un tableau puis lire ensuite le contenu du tableau mais seulement les cases qui m'intéressent, je me perds dans des instructions que je découvre au fur et à mesure et que je ne maîtrise pas.

Voici mon code actuel, tel quel avec des bouts commentés pour essayer des trucs :

char tab[12];
char inByte;
int i;
int a;


char frequency[9];
byte shift = 0;
unsigned long tempfreq;
int n;

long int freq = 12123123;

void setup() {
  // initialize both serial ports:
  Serial.begin(9600);
  Serial1.begin(9600);
}

void loop() {


  // read from port 1, send to port 0:
  if (Serial1.available()) {
    inByte = Serial1.read();
    Serial.write(inByte);
    if (inByte == ';')
    {
      Serial.println();
    }
  }

  /*

     if (Serial1.available())
    {
      inByte = Serial1.read();
      tab[i] = inByte;
      i++;
      delay(10);
      if(inByte == ';')
      {
        Serial.println();
    //     message =
        for(a=12;a<i;a++)
        {
        Serial.write(tab[a]);
        }
        Serial.println();

        i=-1;
      }
    }

  */



  // read from port 0, send to port 1:
  if (Serial.available()) {
    inByte = Serial.read();
    if (inByte == 'F') {
      Serial1.print("fa");
      Serial1.print(freq);
      Serial1.print(";");
    }
    else {

      Serial1.write(inByte);
    }
  }

}

J'aimerais seulement comprendre comment faire pour me retrouver avec ma fréquence dans une variable, dépourvue des caractères "FA" et ";", et ensuite m'en servir pour mettre à jour la fréquence une fois au début du code puis ensuite dès que l'encodeur est sollicité.

Si quelqu'un a une idée, qu'il n'hésite pas !

tu lis les caractères 1 par 1, tu les places dans un buffet, jusqu'à ce que tu reçoives ';'
le ';' tu ne le mets pas dans le buffet, tu mets 0 (la valeur zéro, pas le caractère '0') à la place, ce qui indique la fin de chaîne.
Maintenant ta fréquence, exprimée en caractères ASCII, se trouve à l'adresse (buffet+2)
le +2 c'est pour sauter 'F' et 'A'
puis tu utilises la fonction système atoi() (ça veut dire ASCII->Integer)
long freq = atoi ( buffet +2 );

tu pourrais aussi utiliser la fonction Serial. readBytesUntil()

Si la fréquence est du genre 12123123 ça ne tient pas dans un int donc atoi() ne va pas donner la bonne valeur

Utiliser strtoul ou strtoull en fonction de la vraie longueur

Merci à vous deux !

Alors j'ai tenté d' utiliser la fonction Serial. readBytesUntil(), de cette façon :

  // read from port 1, send to port 0:
  if (Serial1.available()) {
    inByte = Serial1.readBytesUntil(';', tab, 8);
    Serial.write(inByte);
    if (inByte == ';')       {
            Serial.println();
          }
        }

mais dans le terminal, maintenant, au lieu d'avoir en retour FA12345678; j'ai 3 carrés.

Du coup j'ai tenté ceci :

  // read from port 1, send to port 0:
  if (Serial1.available()) {
    inByte = Serial1.read();
    tab[i] = inByte;
      i++;
      delay(10);
    //inByte = Serial1.readBytesUntil(';', tab, 8);
    if (inByte == ';')       {
      long freq = atoi ( tab +2 );
      Serial.println(freq);
    }
  }

Mais dans le terminal maintenant j'ai ça :
-4184
-4184

Tu as mal lu la doc :

Serial.readBytesUntil() returns the number of characters read into the buffer. A 0 means that the length parameter <= 0, a time out occurred before any other input, or a termination character was found before any other input.

Salut et merci !
Oui j'ai mal lu la doc car je n'ai pas compris alors j'ai cherché un exemple mais manifestement je ne l'ai pas compris non plus.
Par contre j'ai bien compris la remarque de J-M-L et, vu que j'ai cru comprendre que atoi est Array To Int, j'ai tenté atol (les opticiens) mais à la place du
-4184
-4184
j'ai ça :
0
0

atol() est moins sécurisé que strtoul().
Lis ceci : KooR.fr - strtoull - Langage C

J'ai essayé de comprendre la page que tu me donnes, hbchetti, mais en vain. Je ne saisis pas du tout ce que je dois mettre comme second argument.
De retour sur atol, j'ai fait le ménage dans mon code et j'ignore pourquoi mais ça fonctionne presque.
Je tape "fa;" et j'ai ma fréquence en réponse, dépourvue de FA et de ;.
Bon, ça apparaît deux fois mais ce n'est pas le plus dérangeant.
Mais ça semble figer la connexion car ensuite je peux envoyer fa; plusieurs fois et j'aurais la même réponse même si je change de fréquence sur mon poste.
Tant que j'envoie des fa; j'ai la même réponse. Si j'envoie un F alors mon code envoie normalement fa12123123; mais là ça a seulement pour effet d'éteindre les leds RX et TX de l'ardiono pro micro. Il ne me reste qu'à le redémarrer...
C'est à n'y rien comprendre.

j'ai un autre demi-tuto qui propose une classe qui pourrait vous aider

par exemple avec cette classe StreamDevice essayez

#include "StreamDevice.h"
StreamDevice YaesuFtdx3000(Serial1);

void setup() {
  char tempBuffer[100];
  Serial.begin(115200);
  Serial1.begin(9600);
  YaesuFtdx3000.println(F("FA"));
  YaesuFtdx3000.endCommand(";\r\n", 5000ul, tempBuffer, sizeof tempBuffer);
  
  if (YaesuFtdx3000.awaitKeyword() == KEYWORD_OK)            // this is blocking until timeout or correct answer whichever comes first
    Serial.print(F("J'ai eu une réponse"));
  else
    Serial.print(F("Mauvaise réponse. J'ai eu "));
  Serial.println(tempBuffer);

  if (strncmp(tempBuffer, "FA", 2) == 0) {    // https://cplusplus.com/reference/cstring/strncmp/
    // on a bien reçu FA en début de réponse
    char *endPtr = nullptr;
    unsigned long frequence = strtoul(&(tempBuffer[2]), &endPtr, 10);   // https://cplusplus.com/reference/cstdlib/strtoul/
    if (endPtr && *endPtr == ';') { // on a bien lu tout le texte jusqu'au ;
      Serial.print(F("La fréquence est "));
      Serial.println(frequence);
    } else {
      Serial.println(F("Erreur lecture fréquence"));
    }
  } else {
    Serial.println(F("La réponse ne commence pas par FA"));
  }
}

void loop() {}

PS: cela suppose que le module envoie un \r\n après le ; ➜ si ce n'est pas le cas et qu'il y a juste le ; alors il faut modifier

  YaesuFtdx3000.endCommand(";\r\n", 5000ul, tempBuffer, sizeof tempBuffer);

en

  YaesuFtdx3000.endCommand(";", 5000ul, tempBuffer, sizeof tempBuffer);

PS/ pour que ça marche vous prenez les 2 fichiers StreamDevice.cpp et StreamDevice.h qui sont dans le lien et vous les copiez dans le même répertoire que votre projet, puis vous ouvrez votre projet. Vous devez voir alors 3 onglets dans le projet. Sinon vous pouvez aussi installer StreamDevice dans les bibliothèques

Il y a pourtant plusieurs exemples.
Et J-M-L fournit un exemple utilisant également strtoul().

La grande différence entre atol() et strtoul() est que atol() ne saura pas faire la différence entre FA00000000; et FA;.
atol() renverra ZÉRO dans les deux cas.
strtol() renverra aussi ZÉRO mais le pointeur endPtr aura une valeur différente.

oui, d'où le test

qui dit "si la fonction a pu progresser dans la lecture d'un entier et qu'elle s'est arrêtée sur le ; alors..."

Bonsoir dagui81
Si tu prends l'exemple de l'IDE Arduino 04.Communication/SerialEvent,tu as ce qu'il faut pour recevoir sur un port série. L'adapter pour Serial1

Tu reçois FA14250000
tu supprimes FA par un replace FA par ""
il te reste 14250000 que tu transforme en numérique.

Cordialement
jpbbricole

Bonjour bonjour et merci.
Je tenterai aussi avec serial event.
Que veux-tu dire par <tu supprimes FA par un replace FA par ""> ?
pour strtoul j'ai beau relire la page je ne saisis pas comment ça fonctionne :thinking:

Mais merci pour vos interventions, j'y arriverai bien tôt ou tard.
D'ailleurs sans la communication qui se fige le problème serait déjà résolu.

la classe String a une fonction replace()

donc si vous avez String reponse = "FA14250000;"
vous faites reponse.replace("FA", "");
et vous ne vous trouvez qu'avec "14250000;" dans reponse

(ce genre d'opération est couteuse en mémoire dynamique pendant l'opération donc il faut qu'il reste un peu de RAM dispo)

le second argument de strtoul() est un pointeur sur un pointeur.
DE façon pratique, tu crées un pointeur, une variable de type char*. Inutile de lui donner une valeur.
char* endp;
Après cette ligne, le pointeur existe, mais il pointe ... n'importe où ! peu importe.
Comme 2nd argument de strtoul(), tu passes l'adresse de ce joli pointeur. L'adresse d'un char* est un char**. L'adresse de endp s'écrit &endp.
Donc:
unsigned long valeur = strtoul ( zone_a_traduire, &endp, 10 ); // interpréter en base 10

au retour, la fonction strtoul() a modifié la valeur du pointeur endp. C'est pour ça qu'on passe l'adresse du pointeur.
endp pointe maintenant vers le 1er caractère de la zone à traduire qui n'a pas été utilisé, c-à-d là où strtoul() s'est arrêté d'interpréter les chiffres.
Par exemple, si la zone a traduire contient : 1234ABCB,
endp pointera vers le caractère A, qui en base 10 n'est pas un chiffre.

C'est un poil compliqué, mais utile pour savoir ce que strtoul() a réellement fait, et où continuer pour exploiter les éventuels caractères qui suivent.
Bonne aspirine.

Bonjour dagui81

Voilà l'exemple SerialEvent adapté à ton besoin.
Cette version est faite pour le port Serial de telle façon que tu puisses l'essayer manuellement en introduisant ce que tu reçois de ta liaison RS-232 (FA14250000; par exemple) dans la ligne de commandes du moniteur
image
.
Pour l'adapter à Serial1, il faut changer;
dans setup, le 2ème Serial.begin(9600) en Serial1.begin(9600)
void serialEvent() en void serialEvent1()
while (Serial.available()) en while (Serial1.available())
et
char inChar = (char)Serial.read(); en char inChar = (char)Serial1.read();

Le programme:

// https://forum.arduino.cc/t/comment-extraire-une-partie-dune-trame-recue-par-serial/1042535/1
 // Réception de FA14250000; et extraction de la fréquence



String inputString = "";         // a String to hold incoming data
bool stringComplete = false;  // whether the string is complete

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

  inputString.reserve(50);
}

void loop() 
{
  if (stringComplete)     // Si une chaîne reçue du port séeie
  {
    Serial.println("\nRecu: " + inputString);

	inputString.toUpperCase();    // Tout en majuscules
	if (inputString.startsWith("FA"))
	{
		inputString.replace("FA", "");     // Supprimer FA
		inputString.replace(";", "");     // Supprimer ;
		float frequence = inputString.toFloat();
		Serial.println("Frequence = " + String(frequence));
	} 
	else
	{
		Serial.println("Commande " + inputString + " inconnue!!!");
	}

    inputString = "";
    stringComplete = false;
  }
}

void serialEvent() 
{
  while (Serial.available()) 
  {
    // get the new byte:
    char inChar = (char)Serial.read();
    // add it to the inputString:
    inputString += inChar;
    // if the incoming character is a newline, set a flag so the main loop can
    // do something about it:
    if (inChar == '\n') 
	{
      inputString.trim();     // Nettoyer la chaîne
	  stringComplete = true;
    }
  }
}

Cordialement
jpbbricole

Oh super les gars, vous êtes géniaux !
J'ai hâte de tester ça, je vous tiens au courant :wink:

Bon, j'ai mis le réveil à 5h car il fallait que j'essaie.

Alors pour JPP :
Merci beaucoup pour l'exemple serialEvent adapté à mon besoin mais je n'ai pas réussi à avoir le moindre caractère qui s'affiche, en ayant dûment remplacé les Serial par des Serial1.
C'est probablement moi qui n'ai pas su faire.

Pour JML :
J'arrive à quelque chose avec StreamDevice, j'ai modifié ton code en ceci :

#include "StreamDevice.h"
StreamDevice YaesuFtdx3000(Serial1);

char tempBuffer[100];
char inByte;
long int frequence = 12123123;

void setup() {
  Serial.begin(115200);
  Serial1.begin(38400);
  getfreq();
}

void loop() {

  // read from port 0, send to port 1:
  if (Serial.available()) {
    inByte = Serial.read();
    if (inByte == 'F') {
      Serial1.print("fa");
      Serial1.print(frequence);
      Serial1.print(";");
    }
    else {
      Serial1.write(inByte);
    }
  }
}



void getfreq() {

  YaesuFtdx3000.println(F("fa;"));
  YaesuFtdx3000.endCommand(";\r\n", 5000ul, tempBuffer, sizeof tempBuffer);

  if (YaesuFtdx3000.awaitKeyword() == KEYWORD_OK)    {};        // this is blocking until timeout or correct answer whichever comes first

  if (strncmp(tempBuffer, "FA", 2) == 0) {    // https://cplusplus.com/reference/cstring/strncmp/
    char *endPtr = nullptr;
    unsigned long frequence = strtoul(&(tempBuffer[2]), &endPtr, 10);   // https://cplusplus.com/reference/cstdlib/strtoul/
    if (endPtr && *endPtr == ';')     { // on a bien lu tout le texte jusqu'au ;
      Serial.println(frequence);
    }
  }
}

Puis j'ai modifié la ligne


  YaesuFtdx3000.endCommand(";\r\n", 5000ul, tempBuffer, sizeof tempBuffer);

en


  YaesuFtdx3000.endCommand(";\r\n", 50ul, tempBuffer, sizeof tempBuffer);

car l'idée est ensuite d'avoir un encodeur rotatif qui va modifier la fréquence dans le poste selon un pas défini d'abord dans le code, puis ensuite par 2 BP.

Donc avec ce code j'ai ma fréquence qui s'affiche, j'ai placé un getfreq() dans le loop() et j'ai bien ma fréquence qui se met à jour dès que je touche au bouton de changement de fréquence sur mon poste.
C'est fantastique !

Mais j'ai voulu, dans l'euphorie de la réussite, enchainer avec l'ajout de l'encodeur.
J'ai pris l'exemple "interruptProMicro" de la librairie "Rotary" , je l'ai chargé tel quel dans l'Arduino et j'avais bien la réponse attendue dans le Serial Monitor.
Donc j'ai tenté d'intégrer tout ça dans mon code (enfin, le vôtre) avec les pins de l'encodeur sur les pins 8 et 9 puis le BP sur la pin 10 et ça a donné ça :

#include "StreamDevice.h"
StreamDevice YaesuFtdx3000(Serial1);

char tempBuffer[100];
char inByte;
long int frequence = 12123123;
int BP = digitalRead(10);


#include <Rotary.h>
Rotary r = Rotary(8, 9);



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

  pinMode(BP, INPUT_PULLUP);

  getfreq();

  r.begin();
  PCICR |= (1 << PCIE0);
  PCMSK0 |= (1 << PCINT4) | (1 << PCINT5);
  sei();
}

void loop() {

  // read from port 0, send to port 1:
  if (Serial.available()) {
    inByte = Serial.read();
    if (inByte == 'F') {
      Serial1.print("fa");
      Serial1.print(frequence);
      Serial1.print(";");
    }
    else {
      Serial1.write(inByte);
    }
  }
  //if (BP == LOW) {
  //    getfreq();
  //  }
}



void getfreq() {

  YaesuFtdx3000.println(F("fa;"));
  YaesuFtdx3000.endCommand(";\r\n", 50ul, tempBuffer, sizeof tempBuffer);

  if (YaesuFtdx3000.awaitKeyword() == KEYWORD_OK)    {};        // this is blocking until timeout or correct answer whichever comes first

  if (strncmp(tempBuffer, "FA", 2) == 0) {    // https://cplusplus.com/reference/cstring/strncmp/
    char *endPtr = nullptr;
    unsigned long frequence = strtoul(&(tempBuffer[2]), &endPtr, 10);   // https://cplusplus.com/reference/cstdlib/strtoul/
    if (endPtr && *endPtr == ';')     { // on a bien lu tout le texte jusqu'au ;
      Serial.println(frequence);
    }
  }
}


ISR(PCINT2_vect) {
  unsigned char result = r.process();
  if (result == DIR_NONE) {
    // do nothing
  }
  else if (result == DIR_CW) {
    frequence = (frequence+1000);
    Serial.print("FA");
    Serial.print(frequence);
    Serial.print(";");
  }
  else if (result == DIR_CCW) {
    frequence = (frequence-1000);
    Serial.print("FA");
    Serial.print(frequence);
    Serial.print(";");
  }
}

Seulement voilà, ça ne fonctionne pas, rien ne s'affiche dans le monitor, l'étape suivante était de remplacer les Serial par des Serial1 dans les dernières lignes, pour l'instant ça n'aurait aucun intérêt.
J'ai lu des trucs sur des conflits de ressources entre les différentes bibliothèques, avez-vous un avis ?

essayez la bibliothèque encoder library