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.
- 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)
- minicom, picocom, kermit sous Linux
- teraterm sous Windows
- 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.
- 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 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
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
#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 ESP324. 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
#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
- #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
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
- 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
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
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 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
- tous les exemples précédents
- dual-serial-logger
- tous les exemples précédents
- ESP-spiffs
- tous les exemples précédents
- ESP32-sdfat
- ESP32-sdfat-http-server
- ESP32-spiffs-http-server
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.
Henri
6. Mises à jour
18/03/2019 : exemples de serveurs WEB23/03/2019 : ajout du STM32
15/04/2019 : ajout d'une méthode hexDump
Aucun commentaire:
Enregistrer un commentaire