Domotique et objets connectés à faire soi-même

M5Stack Atomic GPS. Tracker ESP32 TinyGPS++, export GPX sur carte SD, visualisation sur Google Maps ou VSCode

Thumbnails
Meilleur deal à : banggood.com

Le module GPS pour M5Stack Atom est un mini boitier qui combine un module GPS u-blox UBX-M8030-KT et un mini module M5Atom Lite ESP32-PICO-D4 de M5Stack. Pour ce projet, nous allons développer un tracker GPS à l’aide de la librairie Arduino TinyGPS++ qui nous servira à récupérer les données de localisation, le nombre de satellite, l’altitude et la précision de positionnement (HDOP). Les données GPS du tracker seront enregistrées sur une carte microSD au format GPX afin de pouvoir afficher le tracé sur un logiciel de cartographie tel que OpenStreetMap, Google MAP ou Leaflet.

 

Un fichier de statistique est également généré automatiquement par le programme Arduino. Il exporte la distance parcourue en mètres et en kilomètres, la vitesse maxi et moyenne.

Présentation du module GPS M5Atom Lite de M5Stack

Le module GPS de M5Stack est un boitier qui embarque un récepteur GPS UBX-M8030-KT u-blox sur lequel vient s’insérer un Core M5Atom Lite (la version avec un seul point lumineux).

Le package contient les éléments suivants dans un boitier de protection en plastique transparent

Le boitier GPS est un équipé des éléments suivants

Petit regret, le boitier n’embarque pas de batterie LiPo. Pour alimenter le pack, il faudra utiliser une Power Bank, le bloc d’alimentation TailBat de M5Stack vendu séparément ou bricoler sa propre solution par impression 3D.

7.8Score Expert
Module GPS M5Atom Lite

Pack GPS avec mini module ESP32-Pico-D4 M5Atom Lite

Rapport Prix / Spécifications
8
Qualité de fabrication
9
Support logiciel
5
Accessoires
9
POUR
  • Compact
  • Rapidité d'acquisition du signal GPS
CONTRE
  • Absence de batterie LiPo ou d'un connecteur
  • Pack TailBat mal adapté au pack
  • Impossible de paramétrer le GPS

Présentation du boitier M5Atom Lite ESP32-PICO-D4

Le M5Stack Atom Lite est une mini carte de développement très compacte mesurant 24x24x10mm construit autour d’un micro-contrôleur ESP32-PICO-D4 d’Espressif équipé de 4Mo de mémoire flash SPI intégré. Le PICO est une version adaptée à l’électronique vestimentaire ou mobile. Comme son grand frère, il dispose de la connectivité WiFi et Bluetooth.

Voici les principales équipement et caractéristiques techniques de l’Atom Lite

LED RGB Pilote SK6812 sur la broche GPIO27
Emetteur IR Broche GPIO12
Boutons Utilisateur sur la broche GPIO39

RESET

GPIO compatible breadboard G19, G21, G22, G23, G25, G33

3V3, 5V, GND

Ports USB-C pour alimentation et programmation

GROVE (I2C + I/0 + UART) connecteur PH2.0 4 broches. GND, 5V, G26, G32

Antenne WiFi Antenne 3D 2.4G
Température de fonctionnement 0°C à 40°C
Poids 3g
Dimensions 24 x 24 x 10 mm

Plus d’informations sur cette page.

Remarque, l’émetteur IR n’est pas utilisable lorsque le module est installé sur le module GPS.

Récepteur GPS UBX-M8030 u-blox (série M8)

Le récepteur UBX-M8030 est une puce GPS polyvalente de la série u-blox M8 dont voici les principales caractéristiques :

D’un point de vue informatique, le récepteur GPS UBX-M8030 envoie à la fréquence de 10Hz (10 fois par secondes) des messages au standard NMEA via plusieurs interfaces (UART, I2C, USB et SPI). M5Stack a opté pour l’interface série UART à 9600 bauds.

Seul regret, il n’y a que la broche TX qui est connectée. Cela signifie qu’il ne sera pas possible (sans modifier la carte) d’envoyer des commandes de configuration. Cela peut être bloquant pour certaines application mais au quotidien, ça fonctionne très bien.

Ressources utiles

Voici quelques liens vers des ressources utiles

Le standard NMEA 0183

Il existe plusieurs constellations de satellites permettant de faire de positionnement

Chaque satellites envoie des trames qui sont une simple chaîne de caractère que le récepteur satellite va récupérer et décoder. Le NMEA 0183 est un standard employé par tous les système de positionnement ce qui a permis le développement de puces polyvalentes multi-systèmes. La norme NMEA 0183 est une spécification pour la communication entre équipements marins, dont les équipements GPS. Elle est définie et contrôlée par la National Marine Electronics Association (NMEA), association américaine de fabricants d’appareils électroniques maritimes.

Chaque système dispose de son préfixe pour l’identifier

Voici un exemple de trame NMEA

$GPGGA,064036.289,4836.5375,N,00740.9373,E,1,04,3.2,200.2,M,,,,0000*0E

$GPGGA          :   Type de trame
074036.289     :   Trame envoyée à 07h40m 36,289s (heure au standard UTC)
4936.5375,N    :   Latitude 49,608958° Nord = 48°36’32.25″ Nord
00740.9373,E  :   Longitude 7,682288° Est = 7°40’56.238″ Est
1                          :   Type de positionnement. 1 pour le GPS
04                       :   Nombre de satellites utilisés pour calculer les coordonnées
3.2                      :   Précision horizontale ou HDOP (Horizontal dilution of precision)
202.2,M            :    Altitude 202.2 mètres
,,,,,0000            :   D’autres informations peuvent être inscrites dans ces champs
*0E                    :    Somme de contrôle de parité, un simple XOR sur les caractères entre $ et *3

Plus d’informations sur cette page Wikipedia.

En récupérant le signal de plusieurs satellites, le récepteur GPS est capable de déterminer par triangulation sa position au sol. Plus il y aura de satellite, meilleur sera la précision de positionnement. C’est le HDOP.

TinyGPSPlus, la librairie idéale pour un Tracker GPS

Il existe plusieurs librairies Arduino compatibles ESP32 / ESP8266 qui permettent de décoder les messages GPS au standard NMEA. Vous trouverez également dans l’exemple AtomicGPS avec la librairie M5Atom une librairie pour décoder les messages NMEA.

Après avoir testé la librairie M5Stack et MicroNMEA de Steve Marple, je vous conseille d’utiliser TinyGPSPlus de Mikal Hart. Le gros avantage de TinyGPS++ est quelle dispose de pleins d’outils :

Créer un projet et installer les librairies ESP32 et M5Stack sur l’IDE Arduino

Avant de commencer, vous devrez déjà installer le SDK Arduino-ESP32 prenant en charge les cartes de développement ESP32 et ESP32-Pico. Tout est expliqué en détail dans ce tutoriel.

Une fois l’IDE Arduino prêt, ouvrez le gestionnaire de librairie et installez les librairies suivantes :

Sélectionner la carte de développement M5Stick-C

Le M5Atom Lite est construit une version allégée de l’ESP32, l’ESP32-PICO. Si vous choisissez une carte de développement ESP32 standard, vous obtiendrez une erreur en téléversant le programme depuis l’IDE Arduino.

Il n’y a pas (encore) de configuration pour les Core M5Atom Lite et M5Atom. Il faut sélectionner le M5Stick-C dans la liste qui embarque également un ESP32-PICO.

Diminuez la vitesse de transfert à 115200 bauds sinon vous obtiendrez une erreur.

Sélectionner le niveau des messages de mise au point, Info par exemple.

Créer un projet et installer les librairies sur PlatformIO

Sur PlatformIO, il y a beaucoup moins de préparatifs. Ouvrez l’écran d’accueil de PlatformIO

Créer un nouveau projet

Dans l’assistant de configuration :

Lorsque le projet est prêt, ouvrez le fichier de configuration platformio.ini et collez la configuration suivante pour installer les librairies. L’installation des librairie débute à chaque sauvegarde du fichier platformio.ini.

[env:m5atom]
platform = espressif32
board = m5stick-c
framework = arduino

monitor_speed = 115200

lib_deps = 
    m5stack/M5Atom @ ^0.0.1
    fastled/FastLED @ ^3.3.3
    mikalhart/TinyGPSPlus @ ^1.0.2
    michalmonday/CSV Parser @ ^0.2.0

build_flags = -DCORE_DEBUG_LEVEL=5

Que contient cette configuration

Un projet de tracker GPS avec export au format GPX

L’objectif de ce projet est d’enregistrer à intervalle régulier les coordonnées ainsi que au format GPX. Le format GPX est un format d’enregistrement de coordonnées GPS standard supporté par tous les logiciels de cartographies. C’est un fichier au format XML dont voici un exemple tiré du projet. Pour en savoir plus sur le format GPX, vous trouverez plein d’infos sur cette page Wikipedia.

<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="domotique-et-objets-connectes.fr" xmlns="http://www.topografix.com/GPX/1/1" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
  <trk>
    <trkseg>
      <trkpt lat="..." lon="...">
        <time>...</time>
        <sat>...</sat>
        <ele>...</ele>
        <hdop>...</hdop>
      </trkpt>
      <trkpt>
        ...
      </trkpt>
    </trkseg>
  </trk>
</gpx>
trk : trace

trkseg : segment

trkpt : coordonnées GPS (latitude, longitude)

time : horodatage au format ISO 8601

sat : nombre de satellites

ele : altitude

hdop : précision

 

Plus la fréquence d’acquisition augmente, plus le risque de détruire la carte micro SD augmente lorsqu’on souhaite récupérer les données. Plutôt que de couper brutalement l’alimentation en retirant la batterie par exemple, il suffira d’appuyer une fois sur le bouton utilisateur (broche GPIO39 de l’ESP32) et d’attendre que la LED clignote deux fois en orange.

Pour reprendre l’enregistrement, insérer la carte micro SD dans le lecteur et faites un Reset à l’aide du bouton latéral.

Paramètres du programme Arduino

Voici les paramètres que vous pouvez ajuster en fonction de vos besoins. La valeur entre (parenthèses) est la valeur par défaut.

Comment utiliser la librairie TinyGPS++

Le récepteur GPS envoie en permanence sur le port série (UART) les messages. Pour récupérer ces chaînes, on va donc ouvrir un second port série et “passer” les chaînes à la librairie TinyGPS++ qui va s’occuper de décoder tout ça.

On créé un objet de type TinyGPSPlus qui contient toutes les méthodes et les données GPS ainsi qu’un deuxième port série (le premier étant utilisé pour téléverser et mettre au point le programme). Le nom n’a aucune importance.

TinyGPSPlus gps;
HardwareSerial gps_uart(1);

Dans le setup(), il suffira ensuite d’ouvrir le port série sur la broche 22. La broche RX du module GPS n’étant pas connectée, on passe la valeur -1 comme paramètre.

gps_uart.begin(9600,SERIAL_8N1,22,-1);

Ensuite, il suffit de récupérer à intervalle régulier (par exemple chaque seconde) les messages envoyés par le module GPS dans la loop().

loop()
{
  readGPS();
  delay(1000);
}
static void readGPS(unsigned long ms)
{
  do 
  {
    while (gps_uart.available())
      gps.encode(gps_uart.read());  
  } while (millis() - start < ms);
}

Comment récupérer les données GPS avec TinyGPS++ (API)

Les données GPS sont regroupés dans la librairie TinyGPS++ dans les objets suivants. Chaque objet dispose d’une méthode isValid() qui permet de contrôler que les valeurs décodées sont correctes et isUpdated() qui permet de savoir si la valeur a été actualisée depuis la dernière acquisition. Cela permet de réduire le nombre de points GPS dans le fichier GPX.

Par exemple pour récupérer la vitesse du véhicule en km/h, on exécutera

gps.speed.kmph();

En plus de ces méthodes, on peut connaître la distance entre deux points (coordonnées GPS) à l’aide des méthodes

static double distanceBetween(double lat1, double long1, double lat2, double long2)

ou

static double courseTo(double lat1, double long1, double lat2, double long2)

Par exemple, connaissant les coordonnées GPS du point précédent, on pourra estimée la distance parcourue entre deux mesures comme ceci

double _distance = gps.distanceBetween(gps.location.lat(), gps.location.lng(), prev_lat, prev_long);
Serial.print("distance = "); Serial.println(_distance);

Comment créer puis ajouter des données à un fichier GPX ?

Dans l’article “Stocker des données sur une carte micro SD. Code Arduino compatible ESP32, ESP8266”, nous avons vu les méthodes de base de stocker des données sur une carte SD. C’est finalement aussi simple que d’imprimer du texte sur le moniteur série.

Ici, on souhaite ajouter des données à un fichier existant à chaque fois qu’on récupère de nouvelles coordonnées GPS. Pour ajouter un nouveau point au fichier GPX (qui est un format XML), l’astuce consiste à déplacer le pointeur à l’endroit ou l’on souhaite insérer de nouvelles données à l’aide de la fonction seek().

<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="domotique-et-objets-connectes.fr" xmlns="http://www.topografix.com/GPX/1/1" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
  <trk>
    <trkseg>
      <trkpt lat="..." lon="..."></trkpt>
      <trkpt lat="..." lon="..."></trkpt>
      <trkpt lat="..." lon="..."></trkpt>
            <--------------------------------- POINTEUR
    </trkseg>
  </trk>
</gpx>

De cette façon, on aura pas besoin de stocker en mémoire une grande quantité de données. C’est beaucoup plus efficace que le JSON qui stocke les données sur la mémoire flash.

Il ne faut jamais oublier que la mémoire flash a un nombre limité de cycle d’écriture qui n’est pas adapté à un système d’acquisition de données.

Pour positionner le pointeur, il suffit de “mesurer” la taille du fichier à l’aide de la fonction size() et de soustraire la taille utilisé par la chaîne de fin (ici 27 bytes).

unsigned long filesize = gpxFile.size();
filesize -= 27;
gpxFile.seek(filesize);

Puis on écrit les données du nouveau point et la fin du fichier à l’aide de la méthode print(). Pour éviter d’utiliser inutilement la mémoire du micro-contrôleur, on utiliser la macro F.

gpxFile.print(F("<trkpt lat=\"")); 
gpxFile.print(gps.location.lat(),6);
gpxFile.print(F("\" lon=\""));
gpxFile.print(gps.location.lng(),6);
...
gpxFile.print(F("</trkseg>\r\n</trk>\r\n</gpx>\r\n"));
gpxFile.close();

Comment faire clignoter la LED du M5Atom Lite

Si vous exécutez l’exemple livré avec la librairie M5Atom Lite, vous obtiendrez deux erreurs.

FastLed, la librairie n’est installée. Nous avons déjà résolu le problème précédemment

La ligne M5.dis.fillpix(0x00004f); renvoie une erreur car la méthode fillpix() n’est pas exposée par la librairie M5Atom Lite. La méthode fillpix() a été remplacée par la méthode drawpix() qui gère la matrice de LED du M5Atom 2020. Comme il n’y a qu’un seul point lumineux disponible sur le M5Atom Lite, il suffit d’indiquer 0,0 comme coordonnées.

M5.dis.drawpix(0,0,0x00004f);

En théorie, il est possible d’utiliser la palette de couleurs de la librairie FastLED plutôt que de passer la valeur Hexa de la couleur souhaitée. C’est beaucoup plus simple. Cependant j’ai obtenu des résultats inattendus sans pour autant avoir trouvé l’origine du problème. Si vous avez résolu le problème, n’hésitez pas à partager votre solution dans les commentaires en dessous du projet.

M5.dis.drawpix(0,0,CRGB::Orange);

Code du projet de Tracker GPS avec un M5Atom Lite

Voici le code du projet que vous pouvez également récupérer sur GitHub.

#include <Arduino.h>
#include "M5Atom.h"
#include <SPI.h>
#include "FS.h"
#include <SD.h>
#include <TinyGPS++.h>
#include "esp_log.h"
#include <CSV_Parser.h>

#define TIME_ZONE 1             // Time compensation / compensation décalage horaire
#define RECORD_FREQUENCY 5000   // Record frequency  / Fréquence d'enregistrement

#define ACTIVATE_DEEP_SLEEP_ONCLICK true
#define GPS_TIMEOUT    5000     /* GPS acquisition Timeout | Temps autorisé pour acquiérir le signal GPS */
#define DISPLAY_GPS_DATA true   /* Display GPS message | affiche le message du GPS

static const char* TAG = "m5atom_gps";

/****************************/
/*        PROTOTYPES        */
/****************************/
static void readGPS(unsigned long ms);
static void printGPSData();
static void createDataFile();
static void addGPXPoint();
static void blink_led_orange();
static void blink_led_red();
static void blink_led_blue();
static void blink_led_green();
static void updateStatFile();
static void printWakeupReason();

// The TinyGPS++ object
TinyGPSPlus gps;
HardwareSerial gps_uart(1);

bool firstStart = true;         // Create GPX file in first start | créé le fichier GPX au premier démarrage
char filepath_gpx[25];          // GPX file name | nom du fichier GPX
char filepath_stats[25];        // Stat file name | nom du fichier de statistiques
char today_folder[15];          // Today folder | Dossier pour les fichiers du jour
char point_date[22];
float prev_lat = NULL;            // No previous point on startup | Aucun point précédent au démarrage
float prev_long;  
bool ENABLE_GPS = true;
bool DESACTIVATE_GPS = false;
#define FORCE_UPDATE_STATS true
#define SPEED_BUFFER_SIZE 10

typedef struct TODAYSTATS
{
  float dist;
  float speed_max;
  float speed_mean; 
  double  speedbuffer[SPEED_BUFFER_SIZE];
  bool  speedbufferfull = false;
  int   speedbufferpos = 0;
} TODAYSTATS_t;

TODAYSTATS_t today_stats;

void external_button_pressed()
{
  if ( !ENABLE_GPS ) {
    
  } else {
    blink_led_green();
  }
}

void setup() {
    // begin(bool SerialEnable , bool I2CEnable , bool DisplayEnable )
    M5.begin(true,false,true);   

    // Disable WiFi modem to save power
    // Désactive le modem WiFi pour économiser de la batterie
    //esp_wifi_stop();  

    SPI.begin(23,33,19,-1);
    if(!SD.begin(-1, SPI, 40000000)){
      ESP_LOGE(TAG, "initialization failed!");
    } else {
      sdcard_type_t Type = SD.cardType();

        Serial.printf("SDCard Type = %d \r\n",Type);
        Serial.printf("SDCard Size = %d \r\n" , (int)(SD.cardSize()/1024/1024));
    }

    // Open Serial port with GPS module 
    // Ouvre le port série avec le module HPS
    gps_uart.begin(9600,SERIAL_8N1,22,-1);

    delay(250);

    M5.dis.drawpix(0,0,0);
    
    // Disable GPS when pressing M5Atom Lite Button (GPIO39)
    // Désactive le GPS en appuyant sur le bouton du M5Atom Lite (GPIO39)
    pinMode(39, INPUT);
    attachInterrupt(39, [] {
      ENABLE_GPS = !ENABLE_GPS;
      ESP_LOGI(TAG,"Change GPS stat %u", ENABLE_GPS);
      if ( !ENABLE_GPS ) DESACTIVATE_GPS = true;
    }, RISING);

    printWakeupReason();
}

void loop() 
{
  if ( DESACTIVATE_GPS ) {
    DESACTIVATE_GPS = false;
    blink_led_orange();
    blink_led_orange();
    // On met en sommeil le module
    if (ACTIVATE_DEEP_SLEEP_ONCLICK) {
      // Le tacker GPS pourra être 
      ESP_LOGI(TAG,"Wakeup on button activated");
      esp_sleep_enable_ext0_wakeup(GPIO_NUM_39,0);
      esp_deep_sleep_start();
    }
  }
  // Le module GPS est activé
  if ( ENABLE_GPS ) {
    readGPS(500);

    if (gps.location.isValid() && gps.date.isValid() ) {
        if ( firstStart ) {
          createDataFile();
          delay(250);
          firstStart = false;
        } 

        addGPXPoint();
        
    }  
        
    if (millis() > GPS_TIMEOUT && gps.charsProcessed() < 10)
      ESP_LOGW(TAG, "No GPS data received: check wiring or position");
  }

  delay(RECORD_FREQUENCY);
}

static void createDataFile()
{
  // GPX File name
  // Nom du fichier GPX
  sprintf(today_folder, "/%04d-%02d-%02d", gps.date.year(), gps.date.month(), gps.date.day() );
  if (!SD.exists(today_folder)){
    SD.mkdir(today_folder);
    ESP_LOGI(TAG, "Create today folder %s", today_folder);
  } else {
    ESP_LOGI(TAG, "Today folder already exists %s\n", today_folder);
  }
  // GPX file path
  // Chemin vers le fichier de données
  sprintf(filepath_gpx, "%s/track.gpx", today_folder);
  sprintf(filepath_stats, "%s/stats.csv", today_folder);
  ESP_LOGI(TAG, "GPX file path %s\n", filepath_gpx);
  ESP_LOGI(TAG, "Stats file path %s\n", filepath_stats);
  
  // Reload today stats
  // Recharge les statistiques du jour 
  if ( SD.exists(filepath_stats) ) {
    File statsFile = SD.open(filepath_stats, FILE_READ);
    if ( statsFile ) {
      CSV_Parser cp(/*format*/ "sf", /*has_header*/ true, /*delimiter*/ ',');
      cp.readSDfile(filepath_stats);

      // Il faut absolument vérifier que le fichier n'est pas vide, sinon plantage assuré !
      if ( cp.getRowsCount() > 0 ) {
        float *val_col = (float*)cp["value"];

        if (val_col) 
          today_stats.dist        = (float)val_col[0]; 
          today_stats.speed_max   = (float)val_col[1];  
          today_stats.speed_mean  = (float)val_col[2];  
          cp.print();
          //Serial.printf("dist %f | max speed %f | mean speed %f  \n", today_stats.dist, today_stats.speed_max, today_stats.speed_mean);
      } else {
        // Le fichier est probablement corrompu, on le supprime
        ESP_LOGW(TAG,"Stat file removed because probably corrupted. I'll be re-saved next time");
        SD.remove(filepath_stats);
      } 
    }  
  }  

  // Créé le fichier GPX du jour vide s'il n'existe pas  
  if ( !SD.exists(filepath_gpx) ) {
    ESP_LOGI(TAG, "Create new GPX file %s", filepath_gpx);
    // Create GPX file on startup if not exists
    // Créé le fichier GPX s'il n'existe pas
    File gpxFile = SD.open(filepath_gpx, FILE_WRITE);
    // GPX file header
    // Entête du fichier GPX
    if ( gpxFile ) {
      gpxFile.print(F(
        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n"
        "<gpx version=\"1.1\" creator=\"domotique-et-objets-connectes.fr\" xmlns=\"http://www.topografix.com/GPX/1/1\" \r\n"
        "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n"
        "xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd\">\r\n"
        "\t<trk>\r\n<trkseg>\r\n"));  
      gpxFile.print(F("</trkseg>\r\n</trk>\r\n</gpx>\r\n"));
      gpxFile.close();
    } else {
      ESP_LOGW(TAG, "Impossible to open %s GPX file", filepath_gpx);
      blink_led_red();
    }  
  } else {
    ESP_LOGI(TAG, "%s file already exists", filepath_gpx);
  }  
}

/********************************************/
/* LIT LE MESSAGE RENVOYE PAR LE MODULE GPS */
/********************************************/
static void readGPS(unsigned long ms)
{
  ESP_LOGI(TAG, "Read GPS stream");
  bool led_state = true;
  unsigned long start = millis();
  do 
  {
    while (gps_uart.available())
      gps.encode(gps_uart.read());
      //Serial.println( (millis() - start) % 250);
      if ( (millis() - start) % 250 != 80 ) {
        led_state = !led_state;
      }
      if ( led_state ) 
        M5.dis.drawpix(0,0,0x0099ff);
      else
        M5.dis.drawpix(0,0,0);
      
  } while (millis() - start < ms);

  if ( DISPLAY_GPS_DATA ) printGPSData();
  M5.dis.drawpix(0,0,0);
}

static void addGPXPoint()
{
  // Only if speed > 5 km/h, update max/mean speed, distance traveled
  // Uniquement si la vitesse > 5 km/h, actualise vites moyenne/max, distance parcourue
  if ( gps.speed.kmph() > 5 ) {
    // Add new speed point in the buffer
    // Ajoute la vitesse au buffer tournant permettant de calculer la vitesse moyenne
    double current_speed = gps.speed.kmph();

    today_stats.speedbuffer[today_stats.speedbufferpos] = current_speed;
    today_stats.speedbufferpos += 1;

    if ( current_speed > today_stats.speed_max ) today_stats.speed_max = current_speed;
      
    if ( today_stats.speedbufferpos >= 10 ) {
      today_stats.speedbufferpos = 0;
      today_stats.speedbufferfull = true;
    }
      
    if ( today_stats.speedbufferfull ) {
      float speed_mean = 0;
      for (int l = 0; l < SPEED_BUFFER_SIZE; l++)
      {
        speed_mean += today_stats.speedbuffer[l];
        Serial.print("add to mean"); Serial.println(today_stats.speedbuffer[l]);
      }
      Serial.print("speed_mean total"); Serial.println(speed_mean);
      
      speed_mean = speed_mean / SPEED_BUFFER_SIZE;
      today_stats.speed_mean = speed_mean;
    }    
    
    // Estimate distance traveled from the lasted position
    // Estime la distance parcourue depuis la dernière position connue
    if ( prev_lat != NULL ) {
      double _distance = gps.distanceBetween(gps.location.lat(), gps.location.lng(), prev_lat, prev_long);
      Serial.print("distance = "); Serial.println(_distance);
      // Ajoute la distance parcourue si < 1 km
      if ( _distance > 0 ) {
        // Stocke la position actuelle pour la prochaine estimation de distance parcourue
        today_stats.dist += _distance;

        updateStatFile();
      }
      prev_lat = gps.location.lat();
      prev_long = gps.location.lng();
      //ESP_LOGI(TAG,"Distance traveled %d", _distance );
    } else {
      prev_lat = gps.location.lat();
      prev_long = gps.location.lng();
    }
  }

  if ( FORCE_UPDATE_STATS ) updateStatFile();

  sprintf(point_date, "%4d-%02d-%02dT%02d:%02d:%02dZ",gps.date.year(), gps.date.month(), gps.date.day(), gps.time.hour() + TIME_ZONE, gps.time.minute(),gps.time.second());

  File gpxFile = SD.open(filepath_gpx, FILE_WRITE);
  if( !gpxFile ) {
    ESP_LOGW(TAG, "Impossible to open %s GPX file", filepath_gpx);
    blink_led_red();
  } else {
    ESP_LOGI(TAG, "Add new point %s", point_date);
    double _lat = gps.location.lat();
    double _lng = gps.location.lng();
    double _alt = gps.altitude.meters();
    double _hdop = gps.hdop.hdop();
    int    _sat  = gps.satellites.value();

    unsigned long filesize = gpxFile.size();
    // back up the file pointer to just before the closing tags
    filesize -= 27;
    gpxFile.seek(filesize);
    gpxFile.print(F("<trkpt lat=\"")); 
    gpxFile.print(_lat,6);
    gpxFile.print(F("\" lon=\""));
    gpxFile.print(_lng,6);
    gpxFile.println(F("\">"));
    gpxFile.print(F("<time>"));
    gpxFile.print(point_date);
    gpxFile.println(F("</time>"));  
    // Satellites
    gpxFile.print(F("<sat>"));
    gpxFile.print(_sat);
    gpxFile.println(F("</sat>"));    
    // Elevation | Altitude 
    gpxFile.print(F("<ele>")); 
    gpxFile.print(_alt,1);
    gpxFile.print(F("</ele>\r\n<hdop>")); 
    gpxFile.print(_hdop,3);
    gpxFile.println(F("</hdop>\r\n</trkpt>"));
    gpxFile.print(F("</trkseg>\r\n</trk>\r\n</gpx>\r\n"));
    gpxFile.close();

    blink_led_blue();
  }  
}

/*********************************/
/*  CREATE OR UPDATE STAT FILE   */
/* CREE OU ACTUALISE LES STATS   */
/*********************************/
static void updateStatFile()
{
  File statsFile = SD.open(filepath_stats, FILE_WRITE);
  if(!statsFile) {
    ESP_LOGW(TAG, "Impossible to open %s stats file", filepath_stats);
  } else {
    ESP_LOGI(TAG, "Add stats data to file");
    
    statsFile.print(F("key,value,unit\r\n")); 
    // Distance parcourue aujourd'hui en mètres
    statsFile.print(F("distance,"));
    statsFile.print(today_stats.dist);
    statsFile.print(F(",m"));
    statsFile.print(F("\r\n"));
    // Distance parcourue aujourd'hui en km
    statsFile.print(F("distance,"));
    statsFile.print(today_stats.dist / 1000., 2);
    statsFile.print(F(",km"));
    statsFile.print(F("\r\n"));
    // Vitesse maxi aujourd'hui
    statsFile.print(F("speed_max,"));
    statsFile.print(today_stats.speed_max);
    statsFile.print(F(",hm/h"));
    statsFile.print(F("\r\n"));
    // Vitesse moyenne aujourd'hui
    statsFile.print(F("speed_mean,"));
    statsFile.print(today_stats.speed_mean);
    statsFile.print(F(",km/h"));
    statsFile.print(F("\r\n"));
    statsFile.close();
  }  
}
static void printGPSData()
{
  Serial.print(F("Location: ")); 
  if (gps.location.isValid())
  {
    Serial.print(gps.location.lat(), 6);
    Serial.print(F(","));
    Serial.print(gps.location.lng(), 6);
    Serial.print(F(", age:"));
    Serial.print(gps.location.age(), 6);    
    Serial.print(F(", hdop:"));
    Serial.print(gps.hdop.hdop(), 3);
  }
  else
  {
    Serial.print(F("INVALID LOCATION"));
  }

  Serial.print(F(" Date/Time: "));
  if (gps.date.isValid())
  {
    Serial.print(gps.date.month());
    Serial.print(F("/"));
    Serial.print(gps.date.day());
    Serial.print(F("/"));
    Serial.print(gps.date.year());
  }
  else
  {
    Serial.print(F("INVALID DATE"));
  }

  Serial.print(F(" "));
  if (gps.time.isValid())
  {
    if (gps.time.hour() < 10) Serial.print(F("0"));
    Serial.print(gps.time.hour());
    Serial.print(F(":"));
    if (gps.time.minute() < 10) Serial.print(F("0"));
    Serial.print(gps.time.minute());
    Serial.print(F(":"));
    if (gps.time.second() < 10) Serial.print(F("0"));
    Serial.print(gps.time.second());
    Serial.print(F("."));
    //if (gps.time.centisecond() < 10) Serial.print(F("0"));
    //Serial.print(gps.time.centisecond());
  }
  else
  {
    Serial.print(F("INVALID TIME"));
  }

  Serial.print(F(" Altitude (m):"));
  Serial.print(gps.altitude.meters());

  Serial.print(F(" Speed (km/h):"));
  if (gps.speed.isValid() )
  {
    Serial.print(gps.speed.kmph());
  }
  else
  {
    Serial.print(F("INVALID SPEED"));
  }

  Serial.print(F(" Course:"));
  if (gps.course.isValid() )
  {
    Serial.print(gps.course.deg());
  }
  else
  {
    Serial.print(F("INVALID COURSE"));
  }

  Serial.println();
}

static void printWakeupReason()
{
  esp_sleep_wakeup_cause_t wakeup_reason;
  wakeup_reason = esp_sleep_get_wakeup_cause();

  switch(wakeup_reason)
  {
    case ESP_SLEEP_WAKEUP_EXT0 : Serial.println("Wakeup caused by external signal using RTC_IO"); break;
    case ESP_SLEEP_WAKEUP_EXT1 : Serial.println("Wakeup caused by external signal using RTC_CNTL"); break;
    case ESP_SLEEP_WAKEUP_TIMER : Serial.println("Wakeup caused by timer"); break;
    case ESP_SLEEP_WAKEUP_GPIO : Serial.println("Wakeup caused by GPIO"); break;
    case ESP_SLEEP_WAKEUP_UART : Serial.println("Wakeup caused by UART"); break;
    default : Serial.printf("Wakeup was not caused by deep sleep: %d\n",wakeup_reason); break;
  }
}

static void blink_led_orange()
{
  M5.dis.drawpix(0,0,0xe68a00);
  delay(200);
  M5.dis.drawpix(0,0,0);
  delay(200);
}

static void blink_led_red()
{
  M5.dis.drawpix(0,0,0xff3300);
  delay(200);
  M5.dis.drawpix(0,0,0);
  delay(200);
}

static void blink_led_blue()
{
  M5.dis.drawpix(0,0,0x0000cc);
  delay(200);
  M5.dis.drawpix(0,0,0);
  delay(200);
}

static void blink_led_green()
{
  M5.dis.drawpix(0,0,0x00ff00);
  delay(200);
  M5.dis.drawpix(0,0,0);
  delay(200);
}

Fichiers générés par le programme Arduino du Tracker

Chaque trame GPS étant horodatée, le programme créé automatiquement un dossier à la date du jour dès qu’on démarre le Tracker GPS. Ce dossier contient le fichier tracker.gpx qui est votre parcours et le fichier stats.csv (pour Comma Separated Values).

20201220
  |_ tracker.gpx
  |_ stats.csv

Les données statistiques sont séparées par une virgule : distance parcourue en mètres, distance parcourue en km, vitesse maxi en km/h, vitesse moyenne en km/h. Les données sont organisées sur trois colonnes (clé, valeur, unité).

key,value,unit
distance,3566.13,m
distance,3.57,km
speed_max,38.97,hm/h
speed_mean,24.02,km/h

Visualiser un fichier GPX ?

Attention, avant d’extraire la carte microSD du lecteur, n’oubliez pas d’arrêter le GPS en appuyant sur le bouton et en attendant que la LED clignote 2 fois en orange.

Il existe une multitude de site internets qui permettent de tracer des fichiers GPX et de suivre le dénivelé de la randonnée

Si vous utilisez VSCode (Visual Studio Code) pour développer vos projets ESP32 avec PlatformIO, il existe également deux plugins très pratiques (Geo data Viewer et VSCode Map Preview) qui permettent de visualiser des tracés GPX, KML, GeoJSON, CSV… tout cela sans quitter l’éditeur de code. Pratique !

Importer un tracé GPX sur Google Maps

Pour importer un tracé GPX sur Google Maps, vous aurez besoin de disposer d’un compte et de vous y connecter. Allez ensuite sur Google Map et ouvrez le menu latéral pour accéder à vos adresses.

Allez sur l’onglet Cartes puis Créer une carte en bas du menu

Cliquer sur Importer puis glisser le fichier GPX récupéré sur la carte SD.

Google Map supporte également l’importation des formats CSV, XLSX (Microsoft Excel) et KML.

Le tracé s’affiche immédiatement après la fin du transfert du fichier GPX sur les serveurs de Google.

Afficher un fichier GPX directement dans VSCode avec le plugin Geo data Viewer

Si vous n’aimez pas l’idée d’envoyer vos données personnelles sur les serveurs de Google, vous pouvez très bien visualiser vos fichiers GPX directement dans l’éditeur de code VSCode de Microsoft à l’aide du plugin Geo Data Viewer. Ouvrez le menu plugin pour l’installer.

Pour afficher un fichier GPX (ou un autre format supporté), ouvrez le fichier dans l’éditeur puis convoquez le menu avec la combinaison de touches  Ctrl + Alt + M.

Geo Data Viewer ouvre une nouvelle page avec de nombreux outils de visualisation et d’export.

Mises à jour

22/12/2020 Publication de l’article

English version

 

Avez-vous aimé cet article ?
[Total: 0 Moyenne: 0]
Exit mobile version