Gps avec création de fichiers GPX - Version c-string

Bonjour,
Je vous présente un projet que j'ai réalisé avec l'aide de @J-M-L.
Sans lui, il ne serait pas ce qu'il est. Je le remercie pour son aide.
https://forum.arduino.cc/t/gps-avec-fichier-gpx-version-c-string/978295

Principe de fonctionnement :
Enregistrer des sessions de sport (marche, course à pied, vélo…) au format GPX dans la mémoire Flash du µC puis transférer le dernier fichier enregistré vers le PC. A l’aide d’une application comme Décathlon Coach par exemple, on peut télécharger le fichier GPX ainsi transféré (il se trouve dans le dossier « téléchargements » du PC) pour visualiser et mémoriser l’activité sportive.

Le matériel :

Comme vous pouvez le voir, le montage est simple. L'interrupteur en bas à droite permet d'alimenter l'ESP32 avec la batterie Lipo. Le bouton glissière à gauche est relié sur le pin du haut à 3.3v, sur celui du bas à GND et sur celui du milieu à D13 avec une résistance de 22K.
Pour éviter les contraintes de CEM et filtrer les rebonds du bouton poussoir, j'utilise une résistance de pullup externe de 10K et un condensateur de 100 nF comme ceci :
Condensateur anti-rebonds

L'ensemble est léger, imperméable et tient dans la main.

c-string c’est quoi :
C’est une chaîne de caractères, un tableau comportant plusieurs données de type char, dont le dernier élément est un caractère nul '\0' non visible. Celui-ci permet d'acter la fin de la chaîne de caractères, la fin du tableau. Les µC ne disposent pas d’une grande quantité de mémoire, la classe String étant trop gourmande, il faut donc privilégier l’utilisation des c-string qui permet au contraire de s’adapter à ce handicap et de l’optimiser.
Tout le programme est architecturé autour de ce concept. Le principe étant de stocker dans un tableau de char les chaînes de caractères que le module GPS génère sous forme de trames. Puis certains éléments constitutifs de ces chaînes sont extraits également sous forme de chaînes afin de pouvoir élaborer un fichier GPX.

Fonctionnement :

Après avoir activé l’alimentation de l’ESP32 par batterie à l’aide de l’interrupteur marche arrêt :

  • Si l’interrupteur à glissière Enregistrement/Transfert est sur la position enregistrement (LOW), la LED_BUILTIN bleue clignote jusqu’à ce que le FIX des satellites soit effectué par le module GPS. A ce moment précis, cette led bleue devient fixe jusqu’à ce qu’un appui soit effectué sur le bouton poussoir. Dés lors, elle s’éteint et l’enregistrement de la session de sport commence. Un nouvel appui sur le BP arrête l’enregistrement et rallume la led. A ce stade, il convient d’éteindre le µC.

  • Si l’interrupteur Enregistrement/Transfert est sur la position Transfert (HIGH), le réseau wifi est activé, le serveur est initialisé. Pendant environ 10 secondes la led bleue devient fixe et le transfert du dernier fichier GPX est effectué vers l’ordinateur. A ce stade, il convient d’éteindre le µC et de se repositionner en mode enregistrement pour une nouvelle session.

Le principe d’une machine à état est utilisé pour mettre en œuvre ces deux processus bien distincts. L’interrupteur à glissière identifie le mode et la machine à état oriente le déroulement de chacun d’entre eux. Chaque changement de position de cet interrupteur se fait hors alimentation de l’ESP32.

Si trois fichiers ont été enregistrés dans la mémoire Flash (const byte nbFichiersMax = 3;), ils sont supprimés définitivement et trois autres fichiers seront générés successivement les uns après les autres au cours de chaque nouvelle session. Les fichiers doivent systématiquement être téléchargés sur l’ordinateur après chaque session afin qu’ils soient sauvegardés dans le répertoire « téléchargements » de l’ordinateur.
Les Trackpoints sont enregistrés toutes les 3 secondes ( const int PROGMEM sample = 3000;).

Le code :

Le module GPS génère des chaînes de caractères ASCII sur le deuxième port série de mon ESP32 (RX2 – TX2), ce sont des trames ou sentences ou encore phrases NMEA.

Exemples de phrases NMEA :

$GPRMC,161957.00,A,4654.95725,N,00035.52878,E,0.271,,020322,,,A71
$GPVTG,,T,,M,0.271,N,0.503,K,A
21
$GPGGA,161957.00,4654.95725,N,00035.52878,E,1,06,1.52,54.6,M,46.9,M,,64
$GPGSA,A,3,26,29,31,16,25,23,,,,,,,2.98,1.52,2.56
0A
$GPGSV,3,1,10,04,04,292,13,05,15,047,14,09,02,324,,16,43,303,267C
$GPGSV,3,2,10,23,10,139,25,25,09,125,16,26,80,282,34,27,20,262,78
$GPGSV,3,3,10,29,32,066,20,31,33,200,26
74
$GPGLL,4654.95725,N,00035.52878,E,161957.00,A,A
6D

Le programme n’utilise que trois types de sentences (GPRMC, GPGGA et GPGSA).

Toutes les trames NMEA commencent par le caractère $ et se termine par un retour chariot ‘\r’ puis par un retour à la ligne ‘\n’. Ce sont ces délimiteurs qui permettent de les isoler sous la forme d’un tableau de char, une par une, après leur génération successive, de les identifier avec le code et de les traiter.

Voici donc grossièrement et sans entrer dans les détails le principe de fonctionnement du code :

Globalement le programme commence par trier les trames générées par le module GPS qui nous intéressent, puis en extrait les éléments utiles à la constitution du fichier GPX.

Les phrases NMEA sont stockées dans le tableau de char message : char message[tailleMessageMax + 1];

Dés la mise sous tension, si l’interrupteur à glissière est placé sur le mode enregistrement, la procédure traiterMessage() commence le trie entre les phrases GPRMC, GPGGA et GPGSA. Ce trie s’effectue uniquement :

1/ Si une trame a été reçue à l’aide de la fonction booléenne messageRecu() qui renvoie true si elle rencontre le caractère ‘\n’ (détermine la fin d’une sentence). Dans le cas contraire chaque caractère est stocké dans le tableau de char message et la fonction renvoie false jusqu’à ce qu’elle rencontre un retour à la ligne ;

2/ Si le calcul de la somme de contrôle mentionné dans la trame est valide (Voir la fonction booléenne phraseOK(const char *unePhrase)). Le checksum c’est la représentation de deux caractères hexadécimaux d’un XOR de tous les caractères d’une trame entre (mais sans les inclure) le caractère $ et le caractère *. La fonction vérifie donc entre autre l’adéquation entre le checksum inclus dans la phrase et le calcul qu’elle refait while (*ptr != '*') cksum ^= *ptr++;. Cette comparaison s’effectue ainsi : (cksum == ckControl ).

La procédure traiterMessage() sélectionne les trames qui nous intéressent à l’aide de la fonction strncmp puis les transmet à la procédure extraire(char *message, trameType typeRequete) où les variables utiles à l’intérieur des sentences sont elles mêmes stockées dans un autre tableau de char : char *variablesGpx[83]; ce sont des bouts de chaînes extirpés entre les virgules à l’aide de la fonction strsep. Elles sont ensuite affectées à d’autres variables de type char comme heures, dates, sat, alt … Certaines sont utilisées telles quelles, d’autres sont transmises à des procédures ou fonctions pour traitement :
void timeGpx(char *lesDates, char *lesHeures ) formate les chaînes *dates et *heures au format GPX.
char* conversionDecimal (char *latlong, boolean lati) retourne la latitude ou la longitude en degrés décimaux.

Les variables globales extraites des sentences sous la forme de tableaux de char et constitutives du fichier GPX sont donc alimentées de manière permanente en données actualisées par les fonctions et procédures à chaque passage dans la loop :

char latitu [18], longitu[18];
char alti[18], sate[18], hdopss[18];
char dateHeure[25];

A l’aide de la librairie LITTLEFS.h, les éléments constitutifs d’un fichier GPX sont enregistrés toutes les trois secondes dans la mémoire flash de mon ESP32.
Au retour d’une séance de sport les fichiers sont transmis à mon PC avec l’aide de la librairie ESPAsyncWebServer.h.
Les fichiers GPX sont au format XML, il est aisé de les créer avec les procédures :

void initGPX(void); //Initialisation du fichier GPX
void addTrackpt(void); // ajout d'un point de cheminement
void completeGPX(void); // fin de la structure GPX

Voici le code complet :


/* Selon le travail de JML https://forum.arduino.cc/t/gps-avec-fichier-gpx-version-c-string/978295/31
   et de Philipp Biedenkopf https://github.com/PBiedenkopf/ES-Arduino-GPX-datalogger?msclkid=c74329f6a6ad11ecb926df952f2de52d

*/
#include <LITTLEFS.h>
#include <WiFi.h>
#include "ESPAsyncWebServer.h"
#define FORMAT_LITTLEFS_IF_FAILED true

const byte RXD2 = 16;
const byte TXD2 = 17;

const byte tailleMessageMax = 82;
char message[tailleMessageMax + 1]; // +1 car on doit avoir un caractère de fin de chaîne en C, le '\0'
const char marqueurDeFin = '\n';

char *variablesGpx[83];
char latitu [18], longitu[18];
char alti[18], sate[18], hdopss[18];
char dateHeure[25];
boolean reception = false; // fix des satellites


// Défini une adresse IP statique
IPAddress local_IP(196, 161, 2, 56);
// Défini l’adresse IP de la passerelle
IPAddress gateway(122, 178, 0, 2558);
// Défini le masque de sous-réseau
IPAddress subnet(157, 25, 556, 23);

//WIFI
const char* ssid     = "xxxxx";
const char* password = "123456";

//Nom de Fichier;
int fileID = 0;
char GPXFILE[13];
const byte nbFichiersMax = 3; // défini le nombre de fichiers maximum


// Timer - rythme l'ajout des Trackpoints et le clignotement au départ
unsigned long t_start;   // timer pour trkpt - led
unsigned long t_stop;   // timer pour trkpt - led
const int PROGMEM sample = 3000; // rythme enregistrement Trkpt


// objets pour le bouton,l'interrupteur, la led, serveur
const uint8_t buttonPin = 14;
const uint8_t pinInterrupteur = 13;
const uint8_t ledPin = LED_BUILTIN;
boolean ledStatut = LOW;
bool b_gpx_tracking = false; // indicateur pour démarrer l'enregistrement gpx
bool b_gpx_init = false; //  indique si le fichier gpx est initialisé
AsyncWebServer server(80); // serveur

enum trameType : byte {gprmc = 13, gpgga = 15, gpgsa = 18};
enum {TRANSFERT, FIN_TRANSFERT, ARRET, NOFIX, FIXSAT, EN_ATTENTE} etatCourant;  // machine à état

// Déclaration des fonctions et procédures
void initGPX(void); //Initialisation du fichier GPX
void addTrackpt(void); // ajout d'un point de cheminement
void completeGPX(void); // fin de la structure GPX
bool messageRecu();
bool phraseOK(const char *unePhrase);
void traiterMessage();
void extraire(char *message, int types);
void timeGpx(char *lesDates, char *lesHeures );
char* conversionDecimal (char *latlong, boolean lati);
int NombreChiffreAVir (double ChiffreDec);
void initReseauWifi();
void supprime();


void setup() {
  Serial.begin(115200); Serial.println();
  Serial2.begin(9600, SERIAL_8N1, RXD2, TXD2); // initialise la liaison RX-TX sur espressif WROOM 32

  // initialise LITTLEFS
  if (!LITTLEFS.begin(FORMAT_LITTLEFS_IF_FAILED)) {
    Serial.println("ERREUR LITTLEFS ");
    return;
  }
  // initialise  boutton start et ledPin
  pinMode(buttonPin, INPUT); //PULLUP externe 10k + condo 100 nF
  pinMode(pinInterrupteur, INPUT); //pin haut 3.3v - pin milieu D13 + R22K - pin bas gnd
  pinMode(ledPin, OUTPUT);



  t_stop = 0;
  // machine à état
  etatCourant = TRANSFERT;

}

void loop() {

  if (digitalRead(pinInterrupteur) == LOW) traiterMessage(); // call back machine à état - on ne traite qu'en mode GPS

  switch (etatCourant) {
    case TRANSFERT:
      if (digitalRead(pinInterrupteur) == HIGH) {
        initReseauWifi();
        etatCourant = FIN_TRANSFERT;
      }
      else if (digitalRead(pinInterrupteur) == LOW) {
        etatCourant = NOFIX;
      }
      break;

    case FIN_TRANSFERT:
      server.end();
      WiFi.disconnect(true);
      WiFi.mode(WIFI_OFF);
      Serial.print("FIN DU TRANSFERT");
      etatCourant = ARRET;
      break;

    case ARRET:
      Serial.println("FIN DU PROGRAMME");
      break;

    case NOFIX: // pas de fix
      t_start = millis();
      if ((t_start - t_stop) > 500) {
        t_stop = t_start;
        ledStatut = !ledStatut;
        digitalWrite(ledPin, ledStatut);
      }
      if (reception) etatCourant = FIXSAT;
      break;

    case FIXSAT:
      if (reception && !b_gpx_init) {
        digitalWrite(ledPin, HIGH); // On allume la led
        etatCourant = EN_ATTENTE;
      }
      break;


    case EN_ATTENTE :
      if (digitalRead(buttonPin) == LOW && Serial2.available() > 0) {
        if (!b_gpx_tracking) {
          // Défini un nom de fichier non existant
          while (1) {
            sprintf(GPXFILE, "/track%02d.gpx", fileID);
            if (!LITTLEFS.exists(GPXFILE)) {
              break;
            }
            else {
              fileID++;
            }
          }
          if (fileID == nbFichiersMax) {
            supprime();
            fileID = 0;
          }
          sprintf(GPXFILE, "/track%02d.gpx", fileID);
          Serial.print(F("Current gpx file: ")); Serial.print(GPXFILE); Serial.print("\n");
          initGPX(); // initialise l'entête du fichier GPX
          b_gpx_tracking = true;
          b_gpx_init = true; // le fichier GPX est initialisé
          digitalWrite(ledPin, LOW);
          //delay(300);
        }
        else {
          b_gpx_tracking = false;
          digitalWrite(ledPin, HIGH);
          //delay(300);
        }

        while (digitalRead(buttonPin) == LOW) {
          delay(50);
        }

      }

      if (Serial2.available() > 0) {
        // Si les données data sont disponibles via la connection serie

        // Enregistre uniquement si le délais est atteint et si le fix est effectif
        if (millis() - t_start > sample) {
          // enregistre un nouveau Trackpt
          if (b_gpx_tracking) {
            if (reception) {
              addTrackpt();
            }
          }
          //Complete le fichier GPX si l'enregistrement est arrêté aprés avoir été executé une fois
          if (b_gpx_init && !b_gpx_tracking) {
            completeGPX();
            b_gpx_init = false; // le fichier actuel est terminé et n’est plus initialisé
            digitalWrite(ledPin, HIGH); // éteint la led
          }

          // le timer est réinitialisé pour une prochaine itération
          t_start = millis();
        }

      }
      //break; pas de break car on reste dans l'attente permanaente de l'enregistrement de données ou on éteint tout
  } // fin switch

}


//=========================================== Initialise la structure du fichier gpx
void initGPX() {
  Serial.println(F("Initializing GPX file ..."));
  File file = LITTLEFS.open(GPXFILE, FILE_WRITE);

  if (file) {
    // header
    file.print(F("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?>\n"));

    // start of gpx file body
    file.print(F("<gpx version=\"1.1\" creator=\"P. Biedenkopf\" xmlns=\"http://www.topografix.com/GPX/1/1\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n"));

    // metadata
    file.print(F("  <metadata>\n"));
    file.print(F("    <name>")); file.print(GPXFILE); file.print(F("</name>\n"));
    file.print(F("    <desc>WROOM 32 avec RX2 TX2</desc>\n"));
    //gpx.print(F("    <author>\n"));
    //gpx.print(F("      <name>P. Biedenkopf</name>\n"));
    //gpx.print(F("      <email>ph.bied13@gmail.com</email>\n"));
    //gpx.print(F("    </author>\n"));
    file.print(F("  </metadata>\n"));

    // start track and track segment (only single segmented tracks possible)
    file.print(F("  <trk>\n"));
    file.print (F("    <name>")); file.print(GPXFILE); file.print(F("</name>\n"));
    file.print(F("    <desc>WROOM 32 avec RX2 TX2</desc>\n"));
    file.print(F("    <trkseg>\n"));

    file.close();
  }
  else {
    Serial.println(F("Erreur ouverture gpx-file"));
  }
}

//================================================ Création d'un trackpoint dans la syntaxe gpx
void addTrackpt() {
  Serial.println(F("Ajoute un  Trackpoint ..."));
  File file = LITTLEFS.open(GPXFILE, FILE_APPEND);

  if (file) {
    file.print(F("      <trkpt lat=\""));
    file.print(latitu);
    file.print(F("\" lon=\""));
    file.print(longitu);
    file.println(F("\">"));
    file.print(F("        <ele>")); file.print(alti); file.print(F("</ele>\n"));
    Serial.println(dateHeure); // test
    file.print(F("        <time>")); file.print(dateHeure); file.print(F("</time>\n"));
    file.print(F("        <sat>")); file.print(sate); file.print(F("</sat>\n"));
    file.print(F("        <hdop>")); file.print(hdopss); file.print(F( "</hdop>\n"));
    file.print(F("      </trkpt>\n"));
    file.close();
  }
  else {
    Serial.println(F("Erreur ouverture fichier gpx"));
  }
}
//======================================================= Fin de la structure GPX
void completeGPX() {
  Serial.println(F("Completing GPX file ..."));
  File file = LITTLEFS.open(GPXFILE, FILE_APPEND);

  if (file) {
    // Ferme le parcours enregistré, le segment et le corps du fichier gpx
    file.print(F("    </trkseg>\n"));
    file.print(F("  </trk>\n"));
    file.print(F("</gpx>\n"));
    file.close();
  }
  else {
    Serial.println(F("Erreur ouverture fichier gpx"));
  }
}
//====================================================================================
bool messageRecu() {
  static byte indexMessage = 0;
  int r = Serial2.read();
  if (r != -1) { // on a reçu un caractère
    if (r == marqueurDeFin) {
      message[indexMessage] = '\0'; // on termine la c-string
      indexMessage = 0; // on se remet au début pour la prochaine fois
      return true;
    } else {
      if ((r != '\r') && (indexMessage < tailleMessageMax))
        message[indexMessage++] = (char) r; // on stocke le caractère et on passe à la case suivante
    }
  }
  return false;
}
//================================================
bool phraseOK(const char *unePhrase) {
  byte cksum = 0;

  if (*unePhrase != '$') return false;          // une phrase commence toujours par $
  const char* starPtr = strchr(unePhrase, '*');
  if (starPtr != nullptr) {      // on a une étoile donc un CKSUM
    const char *ptr = unePhrase + 1;
    while (*ptr != '*') cksum ^= *ptr++;
    char *endPtr;
    unsigned long ckControl = strtoul (starPtr + 1, &endPtr, 16);
    return (*endPtr == '\0') && (cksum == ckControl);
  }
  return false; // pas d'étoile, donc pas de checksum à vérifier (ce qui est OK dans certains cas mais ici toutes nos phrases sont attendues avec un check sum)
}
//========================================================================
void traiterMessage() {
  if (messageRecu() && phraseOK(message) ) {

    if (strncmp(message, "$GPRMC", 6) == 0) {
      extraire(message, gprmc);
    }
    else if (strncmp(message, "$GPGGA", 6) == 0) {
      extraire(message, gpgga);
    }
    else if (strncmp(message, "$GPGSA", 6) == 0) {
      extraire(message, gpgsa);
    }

  }
}
//============================================================================
void extraire(char *message, trameType typeRequete) { // extrait les champs utiles des phrases GPRMC - GPGGA - GPGSA

  int nbVirgule = 0;
  char *valide;
  char *heures, *dates;
  char *latit, *longit;
  char *alt, *sat, *hdops;
  char *fixSat;
  char* champNmea;


  for (int i = 0; i < typeRequete; i++) {
    champNmea = strsep(&message, ",");
    variablesGpx[nbVirgule++] = champNmea;
  }
  
  if (typeRequete == gprmc) { //GPRMC
    heures = variablesGpx[1];
    valide = variablesGpx[2];
    latit = variablesGpx[3];
    longit = variablesGpx[5];
    dates = variablesGpx[9];

    if (*valide == 'A') { // Statut:  A = données valides, V = données non valides - 3eme champ des phrases NMEA de type GPRMC
      if (*heures != '\0' && *dates != '\0') { // on ne transmet pas de champs vides à la fonction timeGpx
        timeGpx(dates, heures);
      }
      if (*latit != '\0' && *longit != '\0') { // on ne transmet pas de champs vides à la fonction conversionDecimal
        conversionDecimal(latit, 1);
        conversionDecimal(longit, 0);
      }
    } // si statut vaut A

  }
  else if (typeRequete == gpgga) { //GPGGA
    fixSat = variablesGpx[6];
    sat = variablesGpx[7];
    alt = variablesGpx[9];
    //Serial.print(" fixSat : "); Serial.println(fixSat);
    //atoi(fixSat) > 0 ? reception = true : reception = false; // fix des satellites
    (*fixSat == '1') ? reception = true : reception = false; // fix des satellites
    if (*sat != '\0' && atof(sat) != 0) sprintf(sate, "%s", sat); // on ne traîte pas de champs vides et de valeur égale à 0
    if (*alt != '\0' && atof(alt) != 0)  sprintf(alti, "%s", alt); // on ne traîte pas de champs vides et de valeur égale à 00

  }

  else if (typeRequete == gpgsa) { //GPGSA
    hdops = variablesGpx[16];
    if (*hdops != '\0' && atof(hdops) != 99.99 && atof(hdops) != 0) sprintf(hdopss, "%s", hdops); // on ne traîte pas un champ vide et de valeur égale à 0 et de  valeur = 99.99

  }

}
//=======================================Formate la date et l'heure au format GPX
void timeGpx(char *lesDates, char *lesHeures ) {
  sprintf(dateHeure, "20%.2s-%.2s-%.2sT%.2s:%.2s:%.2sZ", &lesDates[4], &lesDates[2], &lesDates[0], &lesHeures[0], &lesHeures[2], &lesHeures[4]);
}
//=======================================Retoune la latitude ou la longitude en degres decimal
char* conversionDecimal (char *latlong, boolean lati)  {
  char degres[4];
  char  minut [9];
  if (lati) {
    strlcpy ( degres, &latlong[0],  3);
    strlcpy ( minut, &latlong[2], sizeof minut );
  }
  else
  {
    strlcpy ( degres, &latlong[0], sizeof degres );
    strlcpy ( minut, &latlong[3], sizeof minut );
  }
  double resultat = ((atoi(degres)) + ((atof(minut) / 60)));
  return lati > 0 ? dtostrf(resultat, 0, NombreChiffreAVir(resultat), latitu) : dtostrf(resultat, 0, NombreChiffreAVir(resultat), longitu);
}

//=======================================supprime les '0' après la virigule lors de la conversion de la  latitude ou de la longitude en degres decimal, retourne le nombre de chiffres après la virgule
int NombreChiffreAVir (double ChiffreDec) { // retourne le nombre de chiffres après la vrigule d'un nombre décimal - précision sur 13 chiffres
  int nb = 0;
  char tmpBuffer[21];

  char *recherchePoint = strchr(dtostrf(ChiffreDec, 0, 13, tmpBuffer), '.');
  if (recherchePoint == nullptr) return 0;

  for (int f = strlen(tmpBuffer) - 1; f >= 0; --f)
    if (tmpBuffer[f] == '0') nb++;
    else break;
  return 13 - nb;
}
//===============================Initialise le reseau Wifi et le serveur
void initReseauWifi () {
  // Quel est le dernier fichier ?
  while (1) {
    sprintf(GPXFILE, "/track%02d.gpx", fileID);
    if (!LITTLEFS.exists(GPXFILE)) {
      break;
    }
    else {
      fileID++;

    }
  }
  sprintf(GPXFILE, "/track%02d.gpx", fileID - 1); // il est là

  // wifi IP statique
  if (!WiFi.config(local_IP, gateway, subnet)) {
    Serial.println("erreur de configuration STA");
  }

  //  WiFi
  Serial.print("Connexion à ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("WiFi connecté");
  Serial.println("addresse IP : ");
  Serial.println(WiFi.localIP());
  //serveur
  server.on("/", HTTP_GET, [](AsyncWebServerRequest * request) {
    request->send(LITTLEFS, GPXFILE, "text/xml", true);
  });
  server.on("/interpret", HTTP_GET, [](AsyncWebServerRequest * request) {
    request->send(LITTLEFS, GPXFILE, "text/xml", false);
  });
  server.begin();
  digitalWrite(ledPin, HIGH);
  delay(10000);
  digitalWrite(ledPin, LOW);
  }

void supprime() { //===============Supprime les 'nbFichiersMax' fichiers
  for (byte i = 0; i < nbFichiersMax; i++) {
    sprintf(GPXFILE, "/track%02d.gpx", i);
    if (LITTLEFS.remove(GPXFILE)) {
      Serial.println("- Fichier supprimé");
    } else {
      Serial.println("- Fichier non supprimé");
    }
  }
}

L'autonomie du GPS de sport est de 5 heures. Je cours 4 fois 1 heure par semaine. Je le recharge donc une fois par semaine avec le micro chargeur lipo sur le montage (j'ai soudé le cavalier de ce chargeur pour un courant de charge à 500 mA).

1 Like

La même chose mais avec une carte SD :


/* Selon le travail de JML https://forum.arduino.cc/t/gps-avec-fichier-gpx-version-c-string/978295/31
   et de Philipp Biedenkopf https://github.com/PBiedenkopf/ES-Arduino-GPX-datalogger?msclkid=c74329f6a6ad11ecb926df952f2de52d


*/
#include <SD.h>
#include "ESPAsyncWebServer.h"
#include <WiFi.h>
const byte RXD2 = 16;
const byte TXD2 = 17;

//WIFI
const char* ssid     = "xxxxx";
const char* password = "123456";

const byte tailleMessageMax = 82;
char message[tailleMessageMax + 1]; // +1 car on doit avoir un caractère de fin de chaîne en C, le '\0'
const char marqueurDeFin = '\n';

char *variablesGpx[83];
char latitu [18], longitu[18];
char alti[18], sate[18], hdopss[18];
char dateHeure[25];
boolean reception = false; // fix des satellites

// Défini une adresse IP statique
IPAddress local_IP(196, 161, 2, 56);
// Défini l’adresse IP de la passerelle
IPAddress gateway(122, 178, 0, 2558);
// Défini le masque de sous-réseau
IPAddress subnet(157, 25, 556, 23);


//  carte sd
File gpx;
int fileID = 0;
char GPXFILE[13];


// Timer - rythme l'ajout des Trackpoints et le clignotement au départ
unsigned long t_start;   // timer for sampling trkpt
unsigned long t_stop;   // timer for sampling trkpt
const int PROGMEM sample = 1000; // sample time enregistrement Trkpt


// objets pour le bouton, led, serveur...
const uint8_t buttonPin = 14;
const uint8_t pinInterrupteur = 13;
const uint8_t ledPin = LED_BUILTIN;
boolean ledStatut = LOW;
bool b_gpx_tracking = false; // flag for starting the gpx logging
bool b_gpx_init = false; // flag that indicates if gpx file is initialized
const uint8_t chipSelect = 5; // Lecteur SD
AsyncWebServer server(80); // serveur

enum trameType : byte {gprmc = 13, gpgga = 15, gpgsa = 18};
enum {TRANSFERT, FIN_TRANSFERT, ARRET, NOFIX, NB_FICHIERS, FIXSAT, EN_ATTENTE} etatCourant; // machine à état

// pour écraser le fichier le plus vieux
const byte nbFichiersMax = 100; // défini le nombre de fichiers maximum
boolean nbAtteint; // détermine  si le nombre de fichiers maximum est atteint
char nmrFichier[5];

// Déclaration des fonctions et procédures
void initGPX(void); //Initialisation du fichier GPX
void addTrackpt(void); // ajout d'un point de cheminement
void completeGPX(void); // fin de la structure GPX
bool messageRecu();
bool phraseOK(const char *unePhrase);
void traiterMessage();
void extraire(char *message, int types);
void timeGpx(char *lesDates, char *lesHeures );
char* conversionDecimal (char *latlong, boolean lati);
int NombreChiffreAVir (double ChiffreDec);
void initReseauWifi();

// Déclaration pour écraser le fichier le plus vieux
bool isnNbTxt();
void suppressionFichiers(int x);
void fichierLePlusVieux_E(int nmr);
char* fichierLePlusVieux_L ();


void setup() {
  Serial.begin(115200); Serial.println();
  Serial2.begin(9600, SERIAL_8N1, RXD2, TXD2); // initialise la liaison RX-TX sur espressif WROOM 32
  // initialize SD card
  Serial.print(F("Initialisation carte SD ..."));
  if (!SD.begin(chipSelect)) {
    Serial.println(F("Echec Initialisation carte"));
    while (1);
  }
  Serial.println("Carte OK");

  // initialise  boutton start et ledPin
  pinMode(buttonPin, INPUT); //PULLUP externe + condo 100 nF
  pinMode(pinInterrupteur, INPUT); //pin haut 3.3v - pin milieu D13 + R22K - pin bas gnd
  pinMode(ledPin, OUTPUT);

  // lance le timer pour la première fois
  //t_start = millis(); // sample time enregistrement Trkpt
  t_stop = 0;
  // machine à état
  etatCourant = TRANSFERT;
  nbAtteint = false;
}

void loop() {

  if (digitalRead(pinInterrupteur) == LOW) traiterMessage(); // call back machine à état - on ne traite qu'en mode GPS


  switch (etatCourant) {

    case TRANSFERT:
      if (digitalRead(pinInterrupteur) == HIGH) {
        initReseauWifi();
        etatCourant = FIN_TRANSFERT;
      }
      else if (digitalRead(pinInterrupteur) == LOW) {
        etatCourant = NOFIX;
      }
      break;

    case FIN_TRANSFERT:
      server.end();
      WiFi.disconnect(true);
      WiFi.mode(WIFI_OFF);
      Serial.print("FIN DU TRANSFERT");
      etatCourant = ARRET;
      break;

    case ARRET:
      Serial.println("FIN DU PROGRAMME");
      break;

    case NOFIX: // pas de fix
      t_start = millis();
      if ((t_start - t_stop) > 500) {
        t_stop = t_start;
        ledStatut = !ledStatut;
        digitalWrite(ledPin, ledStatut);
      }
      if (reception) etatCourant = NB_FICHIERS;
      break;

    case NB_FICHIERS:
      if (isnNbTxt()) {
        fileID = atoi(fichierLePlusVieux_L());
        nbAtteint = true;
      }
      etatCourant = FIXSAT;
      break;

    case FIXSAT:
      if (reception && !b_gpx_init) {
        digitalWrite(ledPin, HIGH); // On allume la led
        etatCourant = EN_ATTENTE;
      }
      break;


    case EN_ATTENTE :
      if (digitalRead(buttonPin) == LOW && Serial2.available() > 0) {
        if (!b_gpx_tracking) {

          if ((fileID < nbFichiersMax) && nbAtteint)  { // le nombre de fichiers max est atteint - on supprime le prochain
            suppressionFichiers((fileID + 1) % 100);
          }

          if ((fileID == nbFichiersMax - 1) && !nbAtteint) { // on atteint pour la 1ère fois le nombre de fichiers max et on repart à zéro
            Serial.print("atteint : 100  = "); Serial.println(fileID);
            suppressionFichiers((fileID + 1) % 100);
            nbAtteint = true;
            fileID = 0;
          }
          if ((fileID == nbFichiersMax - 1) && nbAtteint)  { // le nombre de fichiers max est atteint une nouvelle fois - on supprime le prochain et on repart à zéro
            Serial.print("atteint : 100  = "); Serial.println(fileID);
            suppressionFichiers((fileID + 1) % 100);
            fileID = 0;
          }
          // Défini un nom de fichier non existant
          while (1) {
            sprintf(GPXFILE, "/track%02d.gpx", fileID);
            if (!SD.exists(GPXFILE)) {
              break;
            }
            else {
              fileID++;
            }
          }
          Serial.print(F("Current gpx file: ")); Serial.print(GPXFILE); Serial.print("\n");
          initGPX(); // initialise l'entête du fichier GPX
          b_gpx_tracking = true;
          b_gpx_init = true; // le fichier GPX est initialisé
          digitalWrite(ledPin, LOW);
        }
        else {
          b_gpx_tracking = false;
          digitalWrite(ledPin, HIGH);
        }
        while (digitalRead(buttonPin) == LOW) {
          delay(50);
        }
      }

      // Si les données data sont disponibles via la connection serie
      if (Serial2.available() > 0) {

        // Enregistre uniquement si le délais est atteint et si le fix est effectif
        if (millis() - t_start > sample) {
          // enregistre un nouveau Trackpt
          if (b_gpx_tracking) {
            if (reception) {
              addTrackpt();
            }
          }
          //Complete le fichier GPX si l'enregistrement est arrêté aprés avoir été executé une fois
          if (b_gpx_init && !b_gpx_tracking) {
            completeGPX();
            b_gpx_init = false; // le fichier actuel est terminé et n’est plus initialisé
            digitalWrite(ledPin, HIGH); // allume la led
            if (nbAtteint) fichierLePlusVieux_E(fileID);
          }

          // le timer est réinitialisé pour une prochaine itération
          t_start = millis();
        }

      }
      //break; pas de break car on reste dans l'attente permanaente de l'enregistrement de données ou on éteint tout
  } // fin switch

}


//=========================================== Initialise la structure du fichier gpx
void initGPX() {
  Serial.println(F("Initializing GPX file ..."));
  gpx = SD.open(GPXFILE, FILE_WRITE);

  if (gpx) {
    // header
    gpx.print(F("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?>\n"));

    // start of gpx file body
    gpx.print(F("<gpx version=\"1.1\" creator=\"P. Biedenkopf\" xmlns=\"http://www.topografix.com/GPX/1/1\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n"));

    // metadata
    gpx.print(F("  <metadata>\n"));
    gpx.print(F("    <name>")); gpx.print(GPXFILE); gpx.print(F("</name>\n"));
    gpx.print(F("    <desc>WROOM 32 avec RX2 TX2</desc>\n"));
    //gpx.print(F("    <author>\n"));
    //gpx.print(F("      <name>P. Biedenkopf</name>\n"));
    //gpx.print(F("      <email>ph.bied13@gmail.com</email>\n"));
    //gpx.print(F("    </author>\n"));
    gpx.print(F("  </metadata>\n"));

    // start track and track segment (only single segmented tracks possible)
    gpx.print(F("  <trk>\n"));
    gpx.print (F("    <name>")); gpx.print(GPXFILE); gpx.print(F("</name>\n"));
    gpx.print(F("    <desc>WROOM 32 avec RX2 TX2</desc>\n"));
    gpx.print(F("    <trkseg>\n"));

    gpx.close();
  }
  else {
    Serial.println(F("Error opening gpx-file"));
  }
}

//================================================ Création d'un trackpoint dans la syntaxe gpx
void addTrackpt() {
  Serial.println(F("Ajoute un  Trackpoint ..."));
  gpx = SD.open(GPXFILE, FILE_APPEND);

  if (gpx) {
    gpx.print(F("      <trkpt lat=\""));
    gpx.print(latitu);
    gpx.print(F("\" lon=\""));
    gpx.print(longitu);
    gpx.println(F("\">"));
    gpx.print(F("        <ele>")); gpx.print(alti); gpx.print(F("</ele>\n"));
    Serial.println(dateHeure); // test
    gpx.print(F("        <time>")); gpx.print(dateHeure); gpx.print(F("</time>\n"));
    gpx.print(F("        <sat>")); gpx.print(sate); gpx.print(F("</sat>\n"));
    gpx.print(F("        <hdop>")); gpx.print(hdopss); gpx.print(F( "</hdop>\n"));
    gpx.print(F("      </trkpt>\n"));
    gpx.close();
  }
  else {
    Serial.println(F("Erreur ouverture fichier gpx"));
  }
}
//======================================================= Fin de la structure GPX
void completeGPX() {
  Serial.println(F("Completing GPX file ..."));
  gpx = SD.open(GPXFILE, FILE_APPEND);

  if (gpx) {
    // Ferme le parcours enregistré, le segment et le corps du fichier gpx
    gpx.print(F("    </trkseg>\n"));
    gpx.print(F("  </trk>\n"));
    gpx.print(F("</gpx>\n"));
    gpx.close();
  }
  else {
    Serial.println(F("Error opening gpx-file"));
  }
}
//====================================================================================
bool messageRecu() {
  static byte indexMessage = 0;
  int r = Serial2.read();
  if (r != -1) { // on a reçu un caractère
    if (r == marqueurDeFin) {
      message[indexMessage] = '\0'; // on termine la c-string
      indexMessage = 0; // on se remet au début pour la prochaine fois
      return true;
    } else {
      if ((r != '\r') && (indexMessage < tailleMessageMax))
        message[indexMessage++] = (char) r; // on stocke le caractère et on passe à la case suivante
    }
  }
  return false;
}
//================================================
bool phraseOK(const char *unePhrase) {
  byte cksum = 0;

  if (*unePhrase != '$') return false;          // une phrase commence toujours par $
  const char* starPtr = strchr(unePhrase, '*');
  if (starPtr != nullptr) {      // on a une étoile donc un CKSUM
    const char *ptr = unePhrase + 1;
    while (*ptr != '*') cksum ^= *ptr++;
    char *endPtr;
    unsigned long ckControl = strtoul (starPtr + 1, &endPtr, 16);
    return (*endPtr == '\0') && (cksum == ckControl);
  }
  return false; // pas d'étoile, donc pas de checksum à vérifier (ce qui est OK dans certains cas mais ici toutes nos phrases sont attendues avec un check sum)
}
//========================================================================
void traiterMessage() {
  if (messageRecu() && phraseOK(message) ) {

    if (strncmp(message, "$GPRMC", 6) == 0) {
      extraire(message, gprmc);
    }
    else if (strncmp(message, "$GPGGA", 6) == 0) {
      extraire(message, gpgga);
    }
    else if (strncmp(message, "$GPGSA", 6) == 0) {
      extraire(message, gpgsa);
    }

  }
}
//============================================================================
void extraire(char *message, trameType typeRequete) { // extrait les champs utiles des phrases GPRMC - GPGGA - GPGSA

  int nbVirgule = 0;
  char *valide;
  char *heures, *dates;
  char *latit, *longit;
  char *alt, *sat, *hdops;
  char *fixSat;
  char* champNmea;


  for (int i = 0; i < typeRequete; i++) {
    champNmea = strsep(&message, ",");
    variablesGpx[nbVirgule++] = champNmea;
  }
  
  if (typeRequete == gprmc) { //GPRMC
    heures = variablesGpx[1];
    valide = variablesGpx[2];
    latit = variablesGpx[3];
    longit = variablesGpx[5];
    dates = variablesGpx[9];

    if (*valide == 'A') { // Statut:  A = données valides, V = données non valides - 3eme champ des phrases NMEA de type GPRMC
      if (*heures != '\0' && *dates != '\0') { // on ne transmet pas de champs vides à la fonction timeGpx
        timeGpx(dates, heures);
      }
      if (*latit != '\0' && *longit != '\0') { // on ne transmet pas de champs vides à la fonction conversionDecimal
        conversionDecimal(latit, 1);
        conversionDecimal(longit, 0);
      }
    } // si statut vaut A

  }
  else if (typeRequete == gpgga) { //GPGGA
    fixSat = variablesGpx[6];
    sat = variablesGpx[7];
    alt = variablesGpx[9];
    //Serial.print(" fixSat : "); Serial.println(fixSat);
    //atoi(fixSat) > 0 ? reception = true : reception = false; // fix des satellites
    (*fixSat == '1') ? reception = true : reception = false; // fix des satellites
    if (*sat != '\0' && atof(sat) != 0) sprintf(sate, "%s", sat); // on ne traîte pas de champs vides et de valeur égale à 0
    if (*alt != '\0' && atof(alt) != 0)  sprintf(alti, "%s", alt); // on ne traîte pas de champs vides et de valeur égale à 00

  }

  else if (typeRequete == gpgsa) { //GPGSA
    hdops = variablesGpx[16];
    if (*hdops != '\0' && atof(hdops) != 99.99 && atof(hdops) != 0) sprintf(hdopss, "%s", hdops); // on ne traîte pas un champ vide et de valeur égale à 0 et de  valeur = 99.99

  }

}
//=======================================Formate la date et l'heure au format GPX
void timeGpx(char *lesDates, char *lesHeures ) {
  sprintf(dateHeure, "20%.2s-%.2s-%.2sT%.2s:%.2s:%.2sZ", &lesDates[4], &lesDates[2], &lesDates[0], &lesHeures[0], &lesHeures[2], &lesHeures[4]);
}
//=======================================Retoune la latitude ou la longitude en degres decimal
char* conversionDecimal (char *latlong, boolean lati)  {
  char degres[4];
  char  minut [9];
  if (lati) {
    strlcpy ( degres, &latlong[0],  3);
    strlcpy ( minut, &latlong[2], sizeof minut );
  }
  else
  {
    strlcpy ( degres, &latlong[0], sizeof degres );
    strlcpy ( minut, &latlong[3], sizeof minut );
  }
  double resultat = ((atoi(degres)) + ((atof(minut) / 60)));
  return lati > 0 ? dtostrf(resultat, 0, NombreChiffreAVir(resultat), latitu) : dtostrf(resultat, 0, NombreChiffreAVir(resultat), longitu);
}

//=======================================supprime les '0' après la virigule lors de la conversion de la  latitude ou de la longitude en degres decimal, retourne le nombre de chiffres après la virgule
int NombreChiffreAVir (double ChiffreDec) { // retourne le nombre de chiffres après la vrigule d'un nombre décimal - précision sur 13 chiffres
  int nb = 0;
  char tmpBuffer[21];

  char *recherchePoint = strchr(dtostrf(ChiffreDec, 0, 13, tmpBuffer), '.');
  if (recherchePoint == nullptr) return 0;

  for (int f = strlen(tmpBuffer) - 1; f >= 0; --f)
    if (tmpBuffer[f] == '0') nb++;
    else break;
  return 13 - nb;
}
//===============================Initialise le reseau Wifi et le serveur
void initReseauWifi () {
  // Quel est le dernier fichier ?
  if (isnNbTxt()) {
    fileID = atoi(fichierLePlusVieux_L()); 
    sprintf(GPXFILE, "/track%02d.gpx", fileID); // il est là
  }
  else if (!isnNbTxt()){
  while (1) {
    sprintf(GPXFILE, "/track%02d.gpx", fileID);
    if (!SD.exists(GPXFILE)) {
      break;
    }
    else {
      fileID++;

    }
  }
   sprintf(GPXFILE, "/track%02d.gpx", fileID-1); // il est là
  }
 

  // wifi IP statique
  if (!WiFi.config(local_IP, gateway, subnet)) {
    Serial.println("erreur de configuration STA");
  }

  //  WiFi
  Serial.print("Connexion à ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("WiFi connecté");
  Serial.println("addresse IP : ");
  Serial.println(WiFi.localIP());
  //serveur
  server.on("/", HTTP_GET, [](AsyncWebServerRequest * request) {
    request->send(SD, GPXFILE, "text/xml", true);
  });
  server.on("/interpret", HTTP_GET, [](AsyncWebServerRequest * request) {
    request->send(SD, GPXFILE, "text/xml", false);
  });
  server.begin();
  digitalWrite(ledPin, HIGH);
  delay(10000);
  digitalWrite(ledPin, LOW);
}
//=====================================// supprime un fichier sur la carte SD
void suppressionFichiers(int x) {
  char bufer[13];
  sprintf(bufer,  "/track%02d.gpx", x);
  Serial.print("supprime :"); Serial.println(bufer);
  if (SD.exists(bufer)) SD.remove(bufer);
}
//=======================================// vérifie l'existence du fichier "/nb.txt"
bool isnNbTxt() {
  while (1) {
    if (!SD.exists("/nb.txt")) {
      return false;
      break;
    }
    else {
      return true;
      break;
    }
  }
}

//==================================== écrit une valeur dans le fichier "nb.txt"
void fichierLePlusVieux_E(int nmr) {
  File nombre = SD.open("/nb.txt", FILE_WRITE);
  if (nombre) {
    nombre.print(nmr);
    nombre.close();
    Serial.print("ecrit : "); Serial.println(nmr);
  }
  else {
    Serial.println(F("Erreur d'ouverture nb.txt"));
  }

}
//====================================
char* fichierLePlusVieux_L () { // lit la valeur dans le fichier "/nb.txt"
  byte indexNbe = 0;
  char r;

  File nombre = SD.open("/nb.txt");
  if (nombre) {
    while (nombre.available() > 0 && r != -1) {
      r =  nombre.read() ;
      nmrFichier[indexNbe++] = r;
    }
    nombre.close();

    nmrFichier[indexNbe] = '\0';
    return nmrFichier;
  }
  else {
    Serial.println(F("Erreur ouverture fichier-txt"));
    return nullptr;
  }

}
1 Like

merci pour le partage !

Merci pour les cours.
C'est toujours un plaisir d'apprendre avec vous. Vos tutos sont très bien faits. Vos remarques sont toujours pertinentes. On sent l'excellence dans vos écrits.
Je ne sais pas quel est votre parcours mais il doit être extrêmement brillant !!!
C'est donc moi qui vous remercie, vous êtes un exemple sur ce forum qui a bien de la chance de vous compter parmi ses membres.

Bonne soirée à vous @J-M-L

Bonjour,
J'ai dessoudé la led power de mes ESP32. Finalement ce n’est pas difficile et on y gagne en autonomie... Surtout avec la version carte SD et Transfert.
Voici une dernière version sans transfert. Il faudra enlever et remettre la carte SD à chaque fois.


/* Selon le travail de JML https://forum.arduino.cc/t/gps-avec-fichier-gpx-version-c-string/978295/31
   et de Philipp Biedenkopf https://github.com/PBiedenkopf/ES-Arduino-GPX-datalogger?msclkid=c74329f6a6ad11ecb926df952f2de52d


*/
#include <SD.h>

const byte RXD2 = 16;
const byte TXD2 = 17;

const byte tailleMessageMax = 82;
char message[tailleMessageMax + 1]; // +1 car on doit avoir un caractère de fin de chaîne en C, le '\0'
const char marqueurDeFin = '\n';

char *variablesGpx[83];
char latitu [18], longitu[18];
char alti[18], sate[18], hdopss[18];
char dateHeure[25];
boolean reception = false; // fix des satellites


//  carte sd
File gpx;
int fileID = 0;
char GPXFILE[13];


// Timer - rythme l'ajout des Trackpoints et le clignotement au départ
unsigned long t_start;   // timer for sampling trkpt
unsigned long t_stop;   // timer for sampling trkpt
const int PROGMEM sample = 1000; // sample time enregistrement Trkpt


// objets pour le bouton et la led
const uint8_t buttonPin = 14;
const uint8_t ledPin = LED_BUILTIN;
boolean ledStatut = LOW;
bool b_gpx_tracking = false; // flag for starting the gpx logging
bool b_gpx_init = false; // flag that indicates if gpx file is initialized
const uint8_t chipSelect = 5; // Lecteur SD

enum trameType : byte {gprmc = 13, gpgga = 15, gpgsa = 18};
enum {NOFIX, NB_FICHIERS,FIXSAT, EN_ATTENTE} etatCourant;  // machine à état

// pour écraser le fichier le plus vieux
const byte nbFichiersMax = 100; // défini le nombre de fichiers maximum
boolean nbAtteint; // détermine  si le nombre de fichiers maximum est atteint
char nmrFichier[5]; //

// Déclaration des fonctions et procédures
void initGPX(void); //Initialisation du fichier GPX
void addTrackpt(void); // ajout d'un point de cheminement
void completeGPX(void); // fin de la structure GPX
bool messageRecu();
bool phraseOK(const char *unePhrase);
void traiterMessage();
void extraire(char *message, int types);
void timeGpx(char *lesDates, char *lesHeures );
char* conversionDecimal (char *latlong, boolean lati);
int NombreChiffreAVir (double ChiffreDec);

// Déclaration pour écraser le fichier le plus vieux
bool isnNbTxt();
void suppressionFichiers(int x);
void fichierLePlusVieux_E(int nmr);
char* fichierLePlusVieux_L ();

void setup() {
  Serial.begin(115200); Serial.println();
  Serial2.begin(9600, SERIAL_8N1, RXD2, TXD2); // initialise la liaison RX-TX sur espressif WROOM 32
  // initialize SD card
  Serial.print(F("Initialisation carte SD ..."));
  if (!SD.begin(chipSelect)) {
    Serial.println(F("Echec Initialisation carte"));
    while (1);
  }
  Serial.println("Carte OK");

  // initialise  boutton start et ledPin
  pinMode(buttonPin, INPUT); //PULLUP externe + condo 100 nF
  pinMode(ledPin, OUTPUT);

  // lance le timer pour la première fois
  t_start = millis(); // sample time enregistrement Trkpt
  t_stop = 0;

  // machine à état
  etatCourant = NOFIX;
  nbAtteint = false;
}

void loop() {

  traiterMessage(); // call back machine à état

  switch (etatCourant) {
    case NOFIX: // pas de fix
      t_start = millis();
      if ((t_start - t_stop) > sample) {
        t_stop = t_start;
        ledStatut = !ledStatut;
        digitalWrite(ledPin, ledStatut);
      }
      if (reception) etatCourant = NB_FICHIERS;
      break;

    case NB_FICHIERS:
      if (isnNbTxt()) {
        fileID = atoi(fichierLePlusVieux_L());
        nbAtteint = true;
      }
      etatCourant = FIXSAT;
      break;

    case FIXSAT:
      if (reception && !b_gpx_init) {
        digitalWrite(ledPin, HIGH); // On éteint la led
        etatCourant = EN_ATTENTE;
      }
      break;



    case EN_ATTENTE :
      if (digitalRead(buttonPin) == LOW && Serial2.available() > 0) {
        if (!b_gpx_tracking) {

          if ((fileID < nbFichiersMax) && nbAtteint)  { // le nombre de fichiers max est atteint - on supprime le prochain
            suppressionFichiers((fileID + 1) % 100);
          }
          
          if ((fileID == nbFichiersMax - 1) && !nbAtteint) { // on atteint pour la 1ère fois le nombre de fichiers max et on repart à zéro
            Serial.print("atteint : 100  = "); Serial.println(fileID);
            suppressionFichiers((fileID + 1) % 100);
            nbAtteint = true;
            fileID = 0;
          }
          if ((fileID == nbFichiersMax - 1) && nbAtteint)  { // le nombre de fichiers max est atteint une nouvelle fois - on supprime le prochain et on repart à zéro
            Serial.print("atteint : 100  = "); Serial.println(fileID);
            suppressionFichiers((fileID + 1) % 100);
            fileID = 0;
          }

          while (1) {
            sprintf(GPXFILE, "/track%02d.gpx", fileID);
            if (!SD.exists(GPXFILE)) {
              break;
            }
            else {
              fileID++;

            }
          }

          Serial.print(F("Current gpx file: ")); Serial.print(GPXFILE); Serial.print("\n");
          initGPX(); // initialise l'entête du fichier GPX
          b_gpx_tracking = true;
          b_gpx_init = true; // le fichier GPX est initialisé
          digitalWrite(ledPin, LOW);
        }
        else {
          b_gpx_tracking = false;
          digitalWrite(ledPin, HIGH);
        }

        while (digitalRead(buttonPin) == LOW) {
          delay(50);
        }

      }

      // Si les données data sont disponibles via la connection serie
      if (Serial2.available() > 0) {

        // Enregistre uniquement si le délais est atteint et si le fix est effectif
        if (millis() - t_start > sample) {
          // enregistre un nouveau Trackpt
          if (b_gpx_tracking) {
            if (reception) {
              addTrackpt();
            }
          }
          //Complete le fichier GPX si l'enregistrement est arrêté aprés avoir été executé une fois
          if (b_gpx_init && !b_gpx_tracking) {
            completeGPX();
            b_gpx_init = false; // le fichier actuel est terminé et n’est plus initialisé
            digitalWrite(ledPin, HIGH); // éteint la led
            if (nbAtteint) fichierLePlusVieux_E(fileID);
          }

          // le timer est réinitialisé pour une prochaine itération
          t_start = millis();
        }

      }
      //break; pas de break car on reste dans l'attente permanaente de l'enregistrement de données ou on éteint tout
  } // fin switch

}


//=========================================== Initialise la structure du fichier gpx
void initGPX() {
  Serial.println(F("Initializing GPX file ..."));
  gpx = SD.open(GPXFILE, FILE_WRITE);

  if (gpx) {
    // header
    gpx.print(F("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?>\n"));

    // start of gpx file body
    gpx.print(F("<gpx version=\"1.1\" creator=\"P. Biedenkopf\" xmlns=\"http://www.topografix.com/GPX/1/1\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n"));

    // metadata
    gpx.print(F("  <metadata>\n"));
    gpx.print(F("    <name>")); gpx.print(GPXFILE); gpx.print(F("</name>\n"));
    gpx.print(F("    <desc>WROOM 32 avec RX2 TX2</desc>\n"));
    //gpx.print(F("    <author>\n"));
    //gpx.print(F("      <name>P. Biedenkopf</name>\n"));
    //gpx.print(F("      <email>ph.bied13@gmail.com</email>\n"));
    //gpx.print(F("    </author>\n"));
    gpx.print(F("  </metadata>\n"));

    // start track and track segment (only single segmented tracks possible)
    gpx.print(F("  <trk>\n"));
    gpx.print (F("    <name>")); gpx.print(GPXFILE); gpx.print(F("</name>\n"));
    gpx.print(F("    <desc>WROOM 32 avec RX2 TX2</desc>\n"));
    gpx.print(F("    <trkseg>\n"));

    gpx.close();
  }
  else {
    Serial.println(F("Erreur ouverture gpx-file"));
  }
}

//================================================ Création d'un trackpoint dans la syntaxe gpx
void addTrackpt() {
  Serial.println(F("Ajoute un  Trackpoint ..."));
  gpx = SD.open(GPXFILE, FILE_APPEND);

  if (gpx) {
    gpx.print(F("      <trkpt lat=\""));
    gpx.print(latitu);
    gpx.print(F("\" lon=\""));
    gpx.print(longitu);
    gpx.println(F("\">"));
    gpx.print(F("        <ele>")); gpx.print(alti); gpx.print(F("</ele>\n"));
    Serial.println(dateHeure); // test
    gpx.print(F("        <time>")); gpx.print(dateHeure); gpx.print(F("</time>\n"));
    gpx.print(F("        <sat>")); gpx.print(sate); gpx.print(F("</sat>\n"));
    gpx.print(F("        <hdop>")); gpx.print(hdopss); gpx.print(F( "</hdop>\n"));
    gpx.print(F("      </trkpt>\n"));
    gpx.close();
  }
  else {
    Serial.println(F("Erreur ouverture fichier gpx"));
  }
}
//======================================================= Fin de la structure GPX
void completeGPX() {
  Serial.println(F("Completing GPX file ..."));
  gpx = SD.open(GPXFILE, FILE_APPEND);

  if (gpx) {
    // Ferme le parcours enregistré, le segment et le corps du fichier gpx
    gpx.print(F("    </trkseg>\n"));
    gpx.print(F("  </trk>\n"));
    gpx.print(F("</gpx>\n"));
    gpx.close();
  }
  else {
    Serial.println(F("Erreur ouverture fichier gpx"));
  }
}
//====================================================================================
bool messageRecu() {
  static byte indexMessage = 0;
  int r = Serial2.read();
  if (r != -1) { // on a reçu un caractère
    if (r == marqueurDeFin) {
      message[indexMessage] = '\0'; // on termine la c-string
      indexMessage = 0; // on se remet au début pour la prochaine fois
      return true;
    } else {
      if ((r != '\r') && (indexMessage < tailleMessageMax))
        message[indexMessage++] = (char) r; // on stocke le caractère et on passe à la case suivante
    }
  }
  return false;
}
//================================================
bool phraseOK(const char *unePhrase) {
  byte cksum = 0;

  if (*unePhrase != '$') return false;          // une phrase commence toujours par $
  const char* starPtr = strchr(unePhrase, '*');
  if (starPtr != nullptr) {      // on a une étoile donc un CKSUM
    const char *ptr = unePhrase + 1;
    while (*ptr != '*') cksum ^= *ptr++;
    char *endPtr;
    unsigned long ckControl = strtoul (starPtr + 1, &endPtr, 16);
    return (*endPtr == '\0') && (cksum == ckControl);
  }
  return false; // pas d'étoile, donc pas de checksum à vérifier (ce qui est OK dans certains cas mais ici toutes nos phrases sont attendues avec un check sum)
}
//========================================================================
void traiterMessage() {
  if (messageRecu() && phraseOK(message) ) {

    if (strncmp(message, "$GPRMC", 6) == 0) {
      extraire(message, gprmc);
    }
    else if (strncmp(message, "$GPGGA", 6) == 0) {
      extraire(message, gpgga);
    }
    else if (strncmp(message, "$GPGSA", 6) == 0) {
      extraire(message, gpgsa);
    }

  }
}
//============================================================================
void extraire(char *message, trameType typeRequete) { // extrait les champs utiles des phrases GPRMC - GPGGA - GPGSA

  int nbVirgule = 0;
  char *valide;
  char *heures, *dates;
  char *latit, *longit;
  char *alt, *sat, *hdops;
  char *fixSat;
  char* champNmea;


  for (int i = 0; i < typeRequete; i++) {
    champNmea = strsep(&message, ",");
    variablesGpx[nbVirgule++] = champNmea;
  }
 
  if (typeRequete == gprmc) { //GPRMC
    heures = variablesGpx[1];
    valide = variablesGpx[2];
    latit = variablesGpx[3];
    longit = variablesGpx[5];
    dates = variablesGpx[9];

    if (*valide == 'A') { // Statut:  A = données valides, V = données non valides - 3eme champ des phrases NMEA de type GPRMC
      if (*heures != '\0' && *dates != '\0') { // on ne transmet pas de champs vides à la fonction timeGpx
        timeGpx(dates, heures);
      }
      if (*latit != '\0' && *longit != '\0') { // on ne transmet pas de champs vides à la fonction conversionDecimal
        conversionDecimal(latit, 1);
        conversionDecimal(longit, 0);
      }
    } // si statut vaut A

  }
  else if (typeRequete == gpgga) { //GPGGA
    fixSat = variablesGpx[6];
    sat = variablesGpx[7];
    alt = variablesGpx[9];
    //Serial.print(" fixSat : "); Serial.println(fixSat);
    //atoi(fixSat) > 0 ? reception = true : reception = false; // fix des satellites
    (*fixSat == '1') ? reception = true : reception = false; // fix des satellites
    if (*sat != '\0' && atof(sat) != 0) sprintf(sate, "%s", sat); // on ne traîte pas de champs vides et de valeur égale à 0
    if (*alt != '\0' && atof(alt) != 0)  sprintf(alti, "%s", alt); // on ne traîte pas de champs vides et de valeur égale à 00

  }

  else if (typeRequete == gpgsa) { //GPGSA
    hdops = variablesGpx[16];
    if (*hdops != '\0' && atof(hdops) != 99.99 && atof(hdops) != 0) sprintf(hdopss, "%s", hdops); // on ne traîte pas un champ vide et de valeur égale à 0 et de  valeur = 99.99

  }

}
//=======================================Formate la date et l'heure au format GPX
void timeGpx(char *lesDates, char *lesHeures ) {
  sprintf(dateHeure, "20%.2s-%.2s-%.2sT%.2s:%.2s:%.2sZ", &lesDates[4], &lesDates[2], &lesDates[0], &lesHeures[0], &lesHeures[2], &lesHeures[4]);
}
//=======================================Retoune la latitude ou la longitude en degres decimal
char* conversionDecimal (char *latlong, boolean lati)  {
  char degres[4];
  char  minut [9];
  if (lati) {
    strlcpy ( degres, &latlong[0],  3);
    strlcpy ( minut, &latlong[2], sizeof minut );
  }
  else
  {
    strlcpy ( degres, &latlong[0], sizeof degres );
    strlcpy ( minut, &latlong[3], sizeof minut );
  }
  double resultat = ((atoi(degres)) + ((atof(minut) / 60)));
  return lati > 0 ? dtostrf(resultat, 0, NombreChiffreAVir(resultat), latitu) : dtostrf(resultat, 0, NombreChiffreAVir(resultat), longitu);
}

//=======================================supprime les '0' après la virigule lors de la conversion de la  latitude ou de la longitude en degres decimal, retourne le nombre de chiffres après la virgule
int NombreChiffreAVir (double ChiffreDec) { // retourne le nombre de chiffres après la vrigule d'un nombre décimal - précision sur 13 chiffres
  int nb = 0;
  char tmpBuffer[21];

  char *recherchePoint = strchr(dtostrf(ChiffreDec, 0, 13, tmpBuffer), '.');
  if (recherchePoint == nullptr) return 0;

  for (int f = strlen(tmpBuffer) - 1; f >= 0; --f)
    if (tmpBuffer[f] == '0') nb++;
    else break;
  return 13 - nb;
}
//=====================================// supprime un fichier sur la carte SD
void suppressionFichiers(int x) {
  char bufer[13];
  sprintf(bufer,  "/track%02d.gpx", x);
  Serial.print("supprime :"); Serial.println(bufer);
  if (SD.exists(bufer)) SD.remove(bufer);
}
//=======================================// vérifie l'existence du fichier "/nb.txt"
bool isnNbTxt() {
  while (1) {
    if (!SD.exists("/nb.txt")) {
      return false;
      break;
    }
    else {
      return true;
      break;
    }
  }
}

//==================================== écrit une valeur dans le fichier "nb.txt"
void fichierLePlusVieux_E(int nmr) {
  File nombre = SD.open("/nb.txt", FILE_WRITE);
  if (nombre) {
    nombre.print(nmr);
    nombre.close();
    Serial.print("ecrit : "); Serial.println(nmr);
  }
  else {
    Serial.println(F("Erreur d'ouverture nb.txt"));
  }

}
//====================================
char* fichierLePlusVieux_L () { // lit la valeur dans le fichier "/nb.txt"
  byte indexNbe = 0;
  char r;

  File nombre = SD.open("/nb.txt");
  if (nombre) {
    while (nombre.available() > 0 && r != -1) {
      r =  nombre.read() ;
      nmrFichier[indexNbe++] = r;
    }
    nombre.close();

    nmrFichier[indexNbe] = '\0';
    return nmrFichier;
  }
  else {
    Serial.println(F("Erreur ouverture fichier-txt"));
    return  nullptr;
  }

}

Bonne journée

Bonjour,
Une petite mise à jour :
Le passage de la version 1.0.6 à 2.0.3 concernant le gestionnaire de carte esp32 by Espressif Systems, implique en ce qui concerne l'utilisation de la mémoire flash de passer par la librairie intégrée.
Il faut désormais utiliser la librairie installée dans le nouveau package : \Utilisateur\Documents\ArduinoData\packages\esp32\hardware\esp32\2.0.3\libraries\LittleFS

==> Il suffit de remplacer : #include <LITTLEFS.h>
par #include <LittleFS.h>
Dans le programme il faudra remplacer LITTLEFS.h par LittleFS.h

Bonne journée.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.