Gps avec création de fichiers GPX - classe String

Bonjour,
Avec le code de @jpbbricole et celui de Philipp Biedenkopf (et le mien), j'ai réalisé le projet d'un récepteur GPS pour la pratique de séances sportives (course à pied, marche...) avec l'enregistrement d'un fichier au format GPX sur une carte SD. Ce fichier peut très bien être par exemple importé et mémorisé sur le site de sport de Decathlon, ce qui fait que vous pouvez visualiser vos sessions de sport sur votre PC, MAC ou smartphone.

Merci encore à @jpbbricole et Philipp Biedenkopf. Merci également à JML pour ses explications sur une partie du code de Biedenkopf et à @olaf_grossebaf pour m'avoir donné l'idée et l'envie de travailler sur ce projet.

jpbbricole
Philipp Biedenkopf
JML
olaf_grossebaf

Le matériel :

Je n'ai pas fait de schéma car tout cela est extrêmement simple, je vous mets juste le câblage SPI du lecteur SD avec l'ESP 32. Le bouton poussoir est sur la pin 14 en INPUT_PULLUP, donc pas besoin de résistance externe. Le micro chargeur est bien entendu connecté à vin et gnd de l'esp 32 (j'ai soudé le cavalier pour un courant de charge à 500 mA). Evidemment tout fonctionne en 3.3v.


Il faut utiliser les connexions à droite : ESP32 pin (SPI3).

Le fonctionnement est simple, aprés avoir activé l'interrupteur, si la LED_BUILTIN (bleue) clignote c'est que la latitude et la longitude ne sont pas disponibles ; c'est lorsqu'elle s'éteint que l'on peut appuyer sur le bouton poussoir et lancer une session, auquel cas la led bleue reste allumée. Si on appuie une deuxième fois la session se termine et la led bleue est éteinte. Donc lorsqu'une session est lancée, la led rouge et la led bleue sont allumées :wink:

J'ai testé ce programme à la campagne, pas en ville ! et j'ai pu enregistrer 4 sessions de course à pied d'environ une heure chacune et pour l'instant ma batterie ne me fait pas défaut. Le tracé obtenu est aussi précis que celui que me retourne le site Garmin de ma montre de sport fenix 6.

Le programme est assez simple en fait :
1/ Si on appuie sur le BP et que la connexion avec le GPS est valide, on utilise la procédure ecouteGps(); (premières instructions dans la loop).

2/ ecouteGps(); lance Serial1Event() qui récupère les phrases NMEA jusqu'à ce qu'un caractère de nouvelle ligne soit détecté. Puis les phrases NMEA qui nous intéressent sont reconnues. A chaque reconnaissance un test du checksum est effectué (sauf pour les phrases GPGSA car le test n'est jamais bon) puis la phrase est traitée pour en extraire les variables utiles à la construction du fichier GPX et ceci avec la procédure traitement().

3/ Pour extraire les variables utiles, la procédure traitement() utilise la fonction extraction (String donnees, char separateur, int nmrVirgule) qui utilise elle-même la virgule pour reconnaître l'emplacement au sein d'une phrase.
traitement() en ce qui concerne l'extraction de la latitude et de la longitude lance la fonction conversionDecimal (String latlong, boolean lati) pour convertir les degrés minutes en degrés décimaux.

4/ On revient dans la loop : Si appui sur le BP, un nom de fichier GPX non existant sur la carte est créé. Puis la procédure initGPX() créé l'entête du fichier, la LED_BUILTIN est allumée. Selon un intervalle d'une seconde un trackpoint est écrit dans le fichier (uniquement si la latitude et la longitude sont valides) grâce à la procédure addTrackpt().
Enfin si un deuxième appui est détecté sur le BP le fichier est terminé à l'aide de la fonction completeGPX() et la la LED_BUILTIN est éteinte.

Je suis conscient que tout ceci n'est pas parfait mais voilà ça fonctionne plutôt pas mal à la campagne. Il faudra que je teste en ville. Parfois il faut appuyer une deuxième fois sur le BP pour que le lancement ou la clôture d'une session soit effective mais la led bleue permet toujours de savoir où on en est ! Il faudra que je travaille encore sur ce point et très certainement sur la gestion de la mémoire par le programme !
Je sais que certains ne vont pas aimer mais j'utilise des câbles dupont car je souhaite pouvoir récupérer mon matériel. J'ai fabriqué un prototype avec une boîte en plastique achetée chez action :smiley: en attendant que ma femme m'autorise à m'acheter une imprimante 3D (c'est pas gagné !)



Voici le code :

/* selon le travail de jpbbricolehttps://forum.arduino.cc/t/lecture-octets-dun-module-gps-et-enregistrement-nmea-et-gpx/909043/46
 et de Philipp Biedenkopf https://github.com/PBiedenkopf/ES-Arduino-GPX-datalogger?msclkid=c74329f6a6ad11ecb926df952f2de52d

    

*/

#include <SD.h>

// Variables globales du GPS
String phrase; // $GPGSA, $GPGGA, $GPRMC
String latit; // Latitude
String longit; // Longitude
String alt ; // altitude
String sat ; // satellites
String hdops ; // Dilution de précision horizontale(HDOP)
char dateHeure[25]; // au format GPX
boolean latitude; // paramètre de la fonction conversionDecimal qui permet de savoir si on traite la latitude ou la longitude
boolean reception = false;
String gpsDonnees;     // Données recues
boolean gpsNouvellesDonnees = false;     // Si nouvelles données

const uint8_t chipSelect = 5; // Lecteur SD


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

// Timer - rythme l'ajout des Trackpoints
unsigned long t_start;   // timer for sampling
const int PROGMEM sample = 1000; // sample time [ms]

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

// Declaration 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
void traitement(void); // Récupère les variables globales pour l'ajout d'un point de cheminement
String extraction (String donnees, char separateur, int nmrVirgule); // Fonction qui permet d'extraire les informations contenues dans une phrase NMEA
boolean testChecksum(String donnees); // test la valeur du Checksum contenu à la fin de chaque phrase NMEA
char calculCheckSum(String donnees); // permet le calcul du Checksum
void ecouteGps(void); // Selectionne les phrases et lance leur traitement
String conversionDecimal (String latlong, boolean lati); // converti la latitude et la longitude en degrés décimaux
void Serial1Event(void); // Récupère les phrases NMEA

void setup() {
  // Selectionne la vitesse des ports série
  Serial.begin(9600);
  Serial1.begin(9600, SERIAL_8N1, 16, 17); // initialise la liaison RX2-TX2 sur expressif 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 start button
  pinMode(buttonPin, INPUT_PULLUP);
  pinMode(ledPin, OUTPUT);

  // lance le timer pour la première fois
  t_start = millis();
}

// ------------------------------------------------------------------------
void loop() {


  // Lancement et arrêt de l'enregistrement d'un fichier GPX si la connexion est valide par le biais d'un bouton
  if (digitalRead(buttonPin) == LOW && Serial1.available() > 0) {
    if (!b_gpx_tracking) {
      ecouteGps(); 
      // Défini un nom de fichier non existant
      while (1) {
        sprintf(GPXFILE, "/track%02d.gpx", fileID);
        if (!SD.exists(GPXFILE)) {
          break;
        }
        else {
          fileID++;
        }
      }
      Serial << F("Current gpx file: ") << GPXFILE << "\n";
      initGPX(); // initialize gpx file header
      b_gpx_tracking = true;
      b_gpx_init = true; // gpx file is initialized now
      digitalWrite(ledPin, HIGH);
      delay(300);
    }
    else {
      b_gpx_tracking = false;
      digitalWrite(ledPin, LOW);
      delay(300);
    }
    delay(200);
    while (digitalRead(buttonPin) == LOW) {
      ;
    }

  }

  // Si les données data sont disponibles via la connection serie
  if (Serial1.available() > 0) {
    ecouteGps(); //gps.encode(Serial1.read()); // read gps data

    // log data only if sample time is reached
    if (millis() - t_start > sample) {
      // enregistre un nouveau Trackpt
      if (b_gpx_tracking) {
        if (reception) {addTrackpt();}
      }
      // complete gpx file if recording is stopped after it was executed once
      if (b_gpx_init && !b_gpx_tracking) {
        completeGPX();
        b_gpx_init = false; // current file is completed and not initialized anymore
        digitalWrite(ledPin, LOW); // turn of record-led
      }

      // reset timer for next iteration
      t_start = millis();
    }
  }
}
// ------------------------------------------------------------------------

// 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 << "    <name>" << GPXFILE << "</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 << "    <name>" << GPXFILE << "</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(latit);
    gpx.print(F("\" lon=\""));
    gpx.print(longit);
    gpx.println("\">");
    gpx << "        <ele>" << alt << "</ele>\n";
    Serial.println(dateHeure); // test
    gpx << "        <time>" << dateHeure << "</time>\n";
    gpx << "        <sat>" << sat << "</sat>\n";
    gpx << "        <hdop>" << hdops << "</hdop>\n";
    gpx << "      </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"));
  }
}


/// @brief Active les modèles d’impression de style C++
template<class T> inline Print &operator <<(Print &str, T arg) {
  str.print(arg);
  return str;
}
void traitement()
{
  if (phrase == "$GPGSA") {
    hdops = extraction(gpsDonnees, ',', 16);
  }

  if (phrase == "$GPGGA") {
    latit = extraction(gpsDonnees, ',', 2);
    longit = extraction(gpsDonnees, ',', 4);
    alt = extraction(gpsDonnees, ',', 9); // altitude
    sat = extraction(gpsDonnees, ',', 7); // Satellites

    latit = conversionDecimal(latit, 1);
    latit.replace(" ", "");
    longit = conversionDecimal(longit, 0);
    longit.replace(" ", "");
    
  }

  if (phrase == "$GPRMC")
  {
    String heure = extraction(gpsDonnees, ',', 1);
    String dates = extraction(gpsDonnees, ',', 9);
    String annee = (dates.substring(4, 6));
    String mois = (dates.substring(2, 4));
    String jour = (dates.substring(0, 2));
    String heures = (heure.substring(0, 2));
    String minut = (heure.substring(2, 4));
    String seconde = (heure.substring(4, 6));

   
    sprintf(dateHeure, "20%02s-%02s-%02sT%02s:%02s:%02sZ", annee, mois, jour, heures, minut, seconde);
  }
  
}

String extraction (String donnees, char separateur, int nmrVirgule)
{
  int virguleTrouve = 0;
  int strIndex[] = {0, -1};
  int maxIndex = donnees.length() - 1;

  for (int i = 0; i <= maxIndex && virguleTrouve <= nmrVirgule; i++) {
    if (donnees.charAt(i) == separateur || i == maxIndex) {
      virguleTrouve++;
      strIndex[0] = strIndex[1] + 1;
      strIndex[1] = (i == maxIndex) ? i + 1 : i;
    }
  }

  return virguleTrouve > nmrVirgule ? donnees.substring(strIndex[0], strIndex[1]) : "";
}

boolean testChecksum(String donnees) {
  int maxIndex = donnees.length();
  String calculDuChecksum = String(calculCheckSum(donnees), HEX);
  calculDuChecksum.toUpperCase();
  String ChecksumRecu = donnees.substring(maxIndex - 2);
  //Serial.println(calculDuChecksum); Serial.println(ChecksumRecu);
  return calculDuChecksum == ChecksumRecu ? true  : false;
}

char calculCheckSum(String donnees) {
  int maxIndex = donnees.length(); // on détermine le nombre de caractère de donnes
  String nvellesDonnees = donnees.substring(1, maxIndex - 3); // on extrait les caractères situés entre $ et *
  int nvelIndex = nvellesDonnees.length();// - 1; on détermine le nombre de caractère de la chaîne à traiter pour le Checksum
  char checkS = 0; //le premier XOR est appliqué sur les valeurs binaires de 0 et G
  // fonction
  for (int v = 0; v < nvelIndex; v++) {
    checkS = char(checkS ^ nvellesDonnees.charAt(v)); // on applique le XOR sur tous les caractères jusqu'à la fin de la chaîne
  }
  return checkS;
}

void ecouteGps() {
  Serial1Event();     // Lecture du port du GPS

  if (gpsNouvellesDonnees)     // Si recu de nouvelles données
  {
    if (gpsDonnees.startsWith("$GPGSA"))
    {
      //Serial.println(testChecksum(gpsDonnees));//
      phrase = "$GPGSA";
      //if (testChecksum(gpsDonnees)) {//
      traitement();

      //}//
    }
    if (gpsDonnees.startsWith("$GPGGA"))
    {
      phrase = "$GPGGA";
      if (testChecksum(gpsDonnees)) {
        traitement();

      }
    }
    if (gpsDonnees.startsWith("$GPRMC"))
    {
      phrase = "$GPRMC";
      if (testChecksum(gpsDonnees)) {
        traitement();

      }
    }
    gpsDonnees = "";
    gpsNouvellesDonnees  = false;
  }
}

//=======================================Retoune la latitude ou la longitude en degres decimal
String conversionDecimal (String latlong, boolean lati)  {
  char x[34];
  String degres;
  String  minut;

  if (lati) {
    degres = latlong.substring(0, 2);
    minut = latlong.substring(2, 10);
  }

  else
  {
    degres = latlong.substring(0, 3);
    minut = latlong.substring(3, 11);
  }
  double resultat = degres.toInt() + (minut.toDouble() / 60);
  resultat > 0 ? reception = true : reception = false;
  if (reception==0) {
    digitalWrite(ledPin, HIGH);
    delay(150);
    digitalWrite(ledPin, LOW);
    delay(150);
  }
  return dtostrf(resultat, 32, 28, x);

}
//===================================== Ecoute du port serie du GPS
void Serial1Event()
{
  while (Serial1.available())
  {
    char gpsChar = (char)Serial1.read();     // Char received from IDE monitor
    if (gpsChar == '\n')     // If new line char received = end of command line
    {
      gpsNouvellesDonnees  = true;
    }
    else
    {
      if (gpsChar >= ' ') {
        gpsDonnees += gpsChar; // >= ' ' to avoid not wanted ctrl char.
      }
    }
  }
}

Bonne jounée.

Joli projet @philippe86220 ! Je le déplace dans la section des projets terminés

Merci @lesept
Bonne journée.

Bonjour,

Pour résoudre ce problème et absorber les rebonds, j'ai choisi la solution matériel : un condensateur de 100 nF monté en parallèle avec le bouton.

Anti rebond input pullup

Ce simple condensateur suffit sachant que l'on utilise la résistance interne sur la pin 14 (pinMode(buttonPin, INPUT_PULLUP);).

Pour la gestion de la mémoire, il n'y a pas de soucis avec un Esp32.
En effet, l'utilisation de la classe String bien pratique et facile à utiliser a cependant un très lourd impact au niveau de la gestion de la mémoire mais son utilisation sur ce type de µcontrôleur ne pose pas de problèmes dans ce cas précis ; les très nombreux tests que j'ai pu effectuer en attestent. Il en va autrement pour les µcontrôleurs de type AVR (UNO, NANO...) Leur capacité en terme de mémoire étant nettement moins importante !
Je conseille aux débutants comme moi de lire l'excellent tuto de JML qui évoque ce problème :

J-M-L

Je tiens également à vous prévenir concernant l'utilisation du Micro chargeur Lipo 4410, en effet, la prise JST n'étant pas fournie, il faut en acheter une et les connections peuvent parfois être inversées :


Attention également si vous faîtes le même montage que moi avec des entretoises, il faut agrandir les trous du Micro chargeur avec une perceuse et un petit foret ==> ne fixez pas de vis du côté de la led rouge ou alors pensez à la rogner sinon la tête de la vis étant trop grande, elle vous fera sauter la led !
Idem côté ESP32, pensez à rogner les vis coté boutons (BOOT, EN) sinon ils sauteront ...

Bonne journée.