samedi 16 mars 2019

Arduino, ESP8266, ESP32 et STM32 : une librairie console / logger



Arduino, ESP8266, ESP32 et STM32

une librairie console / logger


Dans cet article je vais vous présenter un développement récent, une librairie composée de deux parties :
  • une partie affichage sur la console, appelée moniteur série dans le monde ARDUINO
  • un logger permettant d'afficher ou d'enregistrer des messages de log.
Cette librairie doit être utilisable sur les plateformes suivants :
  • ARDUINO
  • ESP8266
  • ESP32
  • STM32

1. Les notions de base

Dans un logiciel, lorsque l'on affiche des messages sur une console, ceux-ci peuvent être de différents type :
  • messages à destination de l'utilisateur
  • messages d'erreur, warning, erreur critique
  • messages destinés à déverminer le logiciel (logging)
Ces messages sont affichés sur un terminal. Le moniteur série de l'IDE ARDUINO en est un, mais ce n'est pas le seul possible. Il en existe d'autres :
  • minicom, picocom, kermit sous Linux
  • teraterm sous Windows
  • etc.
Classiquement dans un logiciel en C, si l'application ne dispose pas d'un écran graphique, les messages à destination de l'utilisateur sont affichés via la sortie standard stdout à l'aide de fonctions telles que putchar, puts, printf, etc.

Les messages d'erreur sont affichés via la sortie standard stderr à l'aide de fonctions telles que fprintf, perror, etc.

En langage C, les messages destinés à déverminer le logiciel utilisent des mécanismes peu standardisés. On appelle ces mécanismes des loggers. Sous Linux,on utilise généralement syslog.
Un logger peut diriger les messages à afficher vers la console, un fichier ou une liaison Ethernet.

Un langage moderne comme PYTHON possède un système de logging évolué permettant la création de logs hautement personnalisables :
  • activation dynamique
  • affichage sur la console
  • création de fichiers de logs
  • création de fichiers de logs avec rotation
  • envoi vers une socket
  • envoi vers une base de données 
  • etc.
Un fichier de log, en français "fichier journal", est un fichier texte où l'on enregistre des messages pouvant être consultés ultérieurement :
  • messages de diagnostic
    • traces de debug
    • erreurs
    • problèmes de communication
  • événements
    • démarrage
    • pannes
    • défauts
  • tentatives d'accès  à un serveur WEB
  • etc.
Sur les plateformes ARDUINO on ne dispose pas de ces facilités, à moins d'installer une librairie.
Sur ESP8266 ou ESP32 une sortie de logs est prévue mais elle ne permet pas de créer des fichiers de logs.

La librairie présentée ici se propose de remédier partiellement à ces manques.

2. Les différentes architectures

Les différentes plateformes testées sont :
  • ARDUINO UNO
  • ARDUINO MEGA
  • ESP8266 NodeMCU (ESP12E) 
  • ESP32 VROOM
Je vous propose un tableau pour résumer les différentes possibilités en matière de sortie sur ligne série :

Carte Serial Serial1 Serial2 Serial3 Soft Serial
UNO NANO Oui


Oui
MEGA Oui Oui Oui Oui Oui
ESP8266 Oui Oui (1)

Oui
ESP32 Oui Oui (2) Oui
Oui
STM32 Oui (3) (3) (3)

(1) limité à la ligne TX
(2) toutes les cartes ne possèdent pas les pins appropriées
(3) Tout dépend de la carte utilisée

2.1. HardwareSerial

La classe HardwareSerial repose sur un périphérique natif du microcontrôleur, un UART ou USART. L'envoi et la réception des bits à la bonne cadence sont assurés par l'UART.
Dans le tableau ci-dessus les lignes série Hard correspondent aux colonnes Serial à Serial3.

2.2. SoftwareSerial

La classe SoftwareSerial repose sur une émulation logicielle d'UART. L'envoi et la réception des bits à la bonne cadence sont assurés par du code. Il faut donc installer une librairie.

ARDUINO : https://github.com/PaulStoffregen/SoftwareSerial.git
ESP8266 :  installée par défaut dans le package support
ESP32 : https://github.com/akshaybaweja/SoftwareSerial.git

La librairie SoftwareSerial pour ESP32 doit être installée dans le répertoire où se trouve le package support ESP32 :
Sous Linux :
/home/username/.arduino15/packages/esp32/hardware/esp32/1.0.1/libraries/
Sous Windows :
C:\Users\username\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.1/libraries/

2.3. L'ARDUINO

La sortie standard existe mais il faut lui affecter un flux. On procède comme ceci :

static FILE uartout = {0} ;

static int console_putchar (char c, FILE *stream)
{
  if (c == '\n') Serial.write('\r');
  Serial.write(c);
  return 0;
}

void setup(void)
{
  Serial.begin(115200);
  Serial.println("printf() sur ARDUINO");
  fdev_setup_stream (&uartout, console_putchar, NULL, _FDEV_SETUP_WRITE);
  stdout = &uartout;
}

// Ensuite les fonctions standard putchar, printf et puts sont parfaitement utilisables :

void loop() {
  for (int i = 0 ; i < 10 ; i++) {
    printf("compteur : %d\n", i);
    delay(100);
  }
  delay(10000);
}


L'ARDUINO ne possède aucun mécanisme de sortie de logs. Il faut ajouter une librairie.

Cette page explique comment rendre le logging dépendant d'une directive de compilation :
https://arduino103.blogspot.com/2012/09/define-debug-une-methode-de-debugging.html

Cette page donne explique comment sortir des logs sur un SoftwareSerial :
https://wolfgang-ziegler.com/blog/serial-debugging-on-arduino

Celles-ci permettent d'afficher des messages sur Serial uniquement :
https://github.com/ptlug/Arduino-logging
https://github.com/thijse/Arduino-Log

Celle-ci permet d'afficher des messages sur HardwareSerial ou SoftwareSerial :
https://github.com/mrRobot62/Arduino-logging-library

Une autre librairie beaucoup plus élaborée, permettant d'activer les logs dynamiquement :
https://github.com/JoaoLopesF/SerialDebug.git

2.4. L'ESP8266

Sur ESP8266 la sortie standard s'active comme ceci :

void setup(void)
{
  Serial.setDebugOutput(true);
}

Malheureusement cela ne fonctionne pas pour la sortie Serial1 ou Serial2. Cela fonctionnait pourtant en version 2.2.0 mais cela ne fonctionne plus en 2.5.0.
L'utilisation n'est pas possible avec un SoftwareSerial.

La librairie standard de l'ESP8266 possède une sortie de DEBUG activable par l'IDE ARDUINO :
https://arduino-esp8266.readthedocs.io/en/latest/Troubleshooting/debugging.html

Cette sortie est utilisable par le développeur d'applications :

  DEBUG_ESP_PORT.printf("output from \n", "DEBUG_ESP_PORT");

Cette sortie, lorsqu'elle est redirigée vers Serial1, a un petit défaut. L'affichage d'un '\n' provoque un saut à la ligne suivante mais pas de retour chariot.
En utilisant Serial le comportement est correct.

2.5. L'ESP32

Sur ESP32 la sortie standard s'active comme ceci :

void setup(void)
{
  Serial.setDebugOutput(true);
}

Comme pour l'ESP8266 cela ne fonctionne pas pour la sortie Serial1 ou Serial2.
L'utilisation n'est pas possible avec un SoftwareSerial.

L'ESP32 possède également sa propre librairie de logging :
https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/system/log.html

Cette sortie est utilisable aussi par le développeur d'applications :

#include <esp_log.h>

  static const char *TAG = "example";
  esp_log_level_set(TAG, ESP_LOG_VERBOSE);
  ESP_LOGI(TAG, "output from %s\n", "ESP32_LOG");


Avant la compilation il faudra sélectionner un niveau de log par défaut : voir Outils/Core DebugLevel.

Cette sortie, sur Serial ou lorsqu'elle est redirigée vers Serial2, fonctionne correctement.

3. Le bilan

En résumé que faut-il faire ?
En premier lieu réussir à rediriger la sortie standard vers la sortie série de notre choix, et ceci quelle que soit la plateforme.

On peut utiliser ensuite une des librairie de logging citées au chapitre 2 ou développer une solution.

3.1. La sortie console standard


3.1.1. L'ARDUINO
Cela va être très simple. La sortie standard va être redirigée vers la console choisie à l'aide de la méthode décrite au chapitre 2.
Comme nous écrivons nous-même notre fonction de redirection, elle peut faire un travail très simple :

static FILE uartout = {0} ;

static int console_putchar (char c, FILE *stream)
{
  if (c == '\n') console->write('\r');
  console->write(c);
  return 0;
}


Tout passera donc par cette fonction.

3.1.2. L'ESP8266 et l'ESP32

Nous avons constaté certains défauts de la librairie standard :
  • la redirection de la sortie standard vers Serial1 ou Serial2 ne fonctionne pas
  • la redirection de la sortie standard vers SoftwareSerial n'existe pas
  • la redirection de la sortie log vers Serial1 ne fonctionne pas sur l'ESP8266
  • la redirection de la sortie log vers SoftwareSerial n'existe pas
Comme nous ne pouvons pas intervenir dans la librairie nous allons redéfinir printf, puts et putchar dans un fichier console.h :

#define printf console_printf
#define puts console_puts
#define putchar console_putc


Tout passera donc par ces fonctions. Par contre pour que cela marche il faudra inclure le fichier console.h dans chaque source.

3.1. Le logger

Si l'on a besoin d'un logger ayant les mêmes fonctionnalités que celui de l'ESP32 il faut le développer. L'avantage est qu'il aura une API identique sur les plateformes ARDUINO, ESP8266 et ESP32

4. Développement d'un logger

Peu d'amateurs ARDUINO savent qu'il existe des librairies de logging.

Lorsque l'on veut déverminer son sketch on ajoute généralement des lignes de Serial.println().

Ensuite lorsque le logiciel fonctionne, en principe on retire ces lignes. C'est dommage car on peut fort bien en avoir besoin par la suite pour corriger un bug ou faire une évolution.

4.1. Définir des macros

Une première astuce consiste à définir des macros :

#define LOGS_ACTIVE

#ifdef LOGS_ACTIVE
#define log_print       Serial.print
#define log_println     Serial.println
#else
#define log_print
#define log_println
#endif

void setup()
{
  Serial.begin(115200);
  log_println("***** BOOT *****");
}

void loop()
{
}


Ainsi, au lieu de retirer les lignes d'affichage de logs, on met simplement la définition de LOGS_ACTIVE en commentaire et les logs ne seront plus affichés.

Comment cela fonctionne t-il ? Le préprocesseur évalue ces lignes de la manière suivante :

  log_println("***** BOOT *****");

Si LOG_ACTIVE est défini, lorsque le mot log_println est rencontré il est remplacé par Serial.println. La ligne devient  :

  Serial.println("***** BOOT *****");

Si LOG_ACTIVE nest pas défini, lorsque le mot log_println est rencontré il est remplacé par du vide. La ligne devient  :

  ("***** BOOT *****");

Cette ligne ne comporte pas d'erreur de syntaxe, mais comme elle n'a pas d'utilité, elle ne sera pas compilée. L'effet sera identique à celui obtenu en supprimant la ligne.

4.2. Activation dynamique

Ensuite on peut ajouter la possibilité d'activer ou de désactiver les logs de manière dynamique sur plusieurs niveaux :
  • messages permanents
  • messages de debug
  • messages d'erreur
#include <SoftwareSerial.h>

#if defined ESP8266 || defined(ESP32)
SoftwareSerial swSer(14, 12, false, 256);
#else
SoftwareSerial softSerial(10, 11);
#endif

#define LOGS_ACTIVE

#define LOG_DEBUG     2
#define LOG_ERROR     1

#define log_port Serial

#ifdef LOGS_ACTIVE
int log_level;

void log_setLevel(int level) {
  log_level = level;
}

void log_always(const char *msg)
{
  log_port.println(msg);
}

void log_debug(const char *msg)
{
  if (log_level >= LOG_DEBUG) {
    log_port.println(msg);
  }
}

void log_error(const char *msg)
{
  if (log_level >= LOG_ERROR) {
    log_port.println(msg);
  }
}

#else
#define log_setLevel
#define log_always
#define log_debug
#define log_error
#endif

void setup()
{
  log_port.begin(115200);
  log_always("***** BOOT *****");
  log_setLevel(LOG_DEBUG);
}

void loop()
{
  log_debug("This is a DEBUG information");
  log_error("This is an ERROR information");
  delay(2000);
}


Avec ce code il est facile d'activer les logs sur trois niveaux :
  • log_setLevel(0);                          // voir seulement les messages permanents
  • log_setLevel(LOG_ERROR);    // voir seulement les messages d'erreur
  • log_setLevel(LOG_DEBUG);    // voir tous les messages
Il est également possible d'utiliser une ligne série différente de Serial :
  • #define log_port Serial              // Serial
  • #define log_port Serial1            // Serial1
  • #define log_port Serial2            // Serial2
  • #define log_port Serial3            // Serial3
  • #define log_port sofSerial         // SoftwareSerial
Si l'on travaille en C++ il est également possible d'instancier un objet logger par fichier source et activer ou non les logs pour chacun d'eux.

Et il reste encore la possibilité de mettre la définition de LOGS_ACTIVE en commentaire pour supprimer les logs.

4.3. Expression du besoin

Notre logger, contrairement à la console sera développé sous forme de classe. Il sera instancié soit de manière unique soit dans chaque fichier source de l'application :

Logger LOGGER("test");
// ou
static Logger LOGGER("test");

Il sera possible d'instancier plusieurs loggers bien entendu. Par exemple les logs d'accès d'un serveur WEB seront enregistrés dans des fichiers. Les logs de debug seront simplement affichés.

Les fonctions de sortie de logs de la librairie accepteront des arguments variables, comme printf() ou les fonction de logs ESP32 :

void Logger::debug(const char *format, ...);

Nous allons factoriser tout cela et développer une fonction du type vprintf :

void Logger::vprintf(const char *format, va_list ap);

Les fonctions de logging utiliseront toutes cette fonction et donc tout passera par elle.

Les logs pourront être redirigés vers différents supports physiques. Chaque support sera géré par une classe Handler :
  • handler Stream (console standard, HardwareSerial ou SoftwareSerial)
  • handler de fichier sur SPIFFS
  • handler de fichier sur µSD
Bien entendu il est possible d'écrire un handler personnalisé (socket, syslog, MYSQL, etc.)
La librairie fournit un handler pour créer des fichiers dans le système de fichiers SPIFFS de l'ESP8266 et ESP32
Le logger possédera différentes classes de formatage de message :
  • formatage simple (formateur par défaut)
  • formatage indiquant la source du message
  • formatage avec horodatage par timestamp en milliseconde
  • formatage avec horodatage par date et heure
Bien entendu il est possible d'écrire un formateur personnalisé
Dans le cas où les logs sont générés dans des fichiers, la récupération de ceux-ci sera confiée à un serveur FTP ou HTTP.

4.4. L'application

La première des choses à faire sera, dans l'application, d'initialiser la ou les lignes série choisies :

  Serial.begin(115200);

Ensuite nous passerons l'adresse de cette instance du type Stream aux fonctions d'initialisation de la librairie. La console et le ou les loggers pourront fonctionner avec deux lignes série différentes, ou rediriger les messages vers un fichier.

Cette adresse sera mémorisée par la fonction console_init() dans une variable du type Stream :

Stream *console_output;

Elle sera aussi mémorisée par le constructeur de l'instance du logger ou du handler choisi.

Un exemple avec un logger sur Serial et un logger sur SD :

Logger LOGGER("http");
LogNameFormatter formatter(&LOGGER);
StreamLogHandler streamHandler(&formatter, &Serial);
Logger ACCESS("access");
LogTimeDateFormatter logFormatter(&ACCESS);
SdFatFileLogHandler logHandler(&logFormatter, &SD, "/access.log", 10000, 5);
void setup()
{
  Serial.begin(115200);
  console_init(&Serial);
  printf("ESP32 SDFAT logging WebServer demo\n");
  LOGGER.init(&Serial);  LOGGER.setLevel(DEBUG);
  formatter.setHandler(&streamHandler);  LOGGER.setFormatter(&formatter);
  ACCESS.init(&Serial);
  ACCESS.setLevel(INFO);
  logFormatter.setHandler(&streamHandler);  ACCESS.setFormatter(&logFormatter);
  if (SD.begin(SS)) {
    log_info("SD Card initialized.");
    logFormatter.setHandler(&logHandler);    log_info(F("SdFatFileLogHandler installed"));
  }
}


Dans cet exemple, on passe au formateur logFormatter le handler streamHandler, donc de type Stream.

Si la SD est montée avec succès, le handler est changé pour que les messages du logger ACCESS soient enregistrés. Sinon on continue en affichant sur le handler streamHandler.

Afin de simplifier les choses, comme cela correspond à la majeure partie des utilisations, le logger pourra être créé avec l'adresse d'un Stream en paramètres. Il pourra créer ainsi lui-même son formateur et son hander par défaut.

Un exemple avec un seul logger sur Serial :

Logger LOGGER("test");

void setup()
{
  Serial.begin(115200);
  console_init(&Serial);
  printf("Basic logging demo\n");
  LOGGER.init(&Serial);
  LOGGER.setLevel(DEBUG);
}


5. La librairie

La librairie est disponible ici :
https://bitbucket.org/henri_bachetti/mpp-console-logger.git

Cette page vous donne toutes les informations nécessaires :
https://riton-duino.blogspot.com/p/migration-sous-bitbucket.html

De nombreux exemples d'utilisation sont fournis.

Certains exemples, comme leur nom l'indique, sont dédiés :
  • ESP32-sdfat : logs sur µSD.
  • ESP32-sdfat-http-server : WebServer avec logs sur µSD
  • ESP32-spiffs : logs sur SPIFFS.
  • ESP32-spiffs-http-server : WebServer avec logs sur SPIFFS
  • ESP8266-spiffs : logs sur SPIFFS.
Les serveurs WEB des exemples permettent de visualiser les logs et de les télécharger.

Les exemple suivants sont utilisables sur une UNO, NANO ou MINI :
  • console
  • basic-logger
  • basic-logger-handler
  • name-log-formatter
  • timedate-log-formatter
  • timestamp-log-formatter
  • soft-serial-logger
Les exemple suivants sont utilisables sur une MEGA  ou un STM32 :
  • tous les exemples précédents
  • dual-serial-logger
Les exemple suivants sont utilisables sur un ESP8266 :
  • tous les exemples précédents
  • ESP-spiffs 
Le support SD n'est pas implémenté pour l'ESP8266 dans la librairie, car le renommage des fichiers est absent.
Les exemple suivants sont utilisables sur un ESP32 :
  • tous les exemples précédents
  • ESP32-sdfat
  • ESP32-sdfat-http-server
  • ESP32-spiffs-http-server
Exemple basic-logger sur une UNO :
Le sketch utilise 4478 octets (13%) de FLASH et 418 octets (20%) de RAM.

Exemple soft-serial-logger sur une UNO :
Le sketch utilise 6846 octets (21%) de FLASH et 659 octets (32%) de RAM.

Cordialement
Henri

6. Mises à jour

18/03/2019 : exemples de serveurs WEB
23/03/2019 : ajout du STM32
15/04/2019 : ajout d'une méthode hexDump


Aucun commentaire:

Enregistrer un commentaire