Suivi de valeurs avec application C/C++ en mode console

Préambule

Dans le cadre des projets, vous serez sûrement amenés à afficher des valeurs provenant de capteurs.

De façon à disposer d’une interface de visualisation à la fois agréable et simple à réaliser, on peut mettre à profit l’émulation de terminal VT100 pris en charge par défaut par le terminal Linux (mais aussi par les versions récentes du terminal Windows).

Cette émulation VT100 fait en sorte que certaines séquences de caractères — dîtes séquences de contrôle — ne sont pas affichées mais plutôt interprétées par le terminal pour :

  • déplacer le curseur sur l’écran

  • modifier la taille des lignes (80 ou 132 colonnes)

  • fixer les attributs du texte (couleur, clignotant, gras, vidéo inverse, souligné)

  • utiliser un jeu de caractères semi-graphiques (pour le dessin de formulaires par exemple)

Ces séquences de contrôle permettent alors de construire des IHMs simples mais néanmoins utiles et suffisantes en phase de développement.

Une séquence de contrôle est une suite de caractères qui débute par le caractère ASCII “ESCAPE” (27 en décimal, 0x1b en hexadécimal, 033 en octal).

De la documentation sur ces séquences de contrôle se trouve facilement sur le web. Ex. :

L’envoi de ces séquences de contrôle en C/C++ sur le terminal se fait à l’aide d’un simple printf() ou std::cout().

vt100.c
#include <iostream>

using namespace std;

int main(void) {
    // Efface l'écran
    cout << "\x1b[2J";
    // Positionne le curseur sur la 1ère colonne de la 2ème ligne
    cout << "\x1b[2;1H";
    // Affiche "Hello" en rouge sur fond noir
    // à la position actuelle du curseur (ligne 2, colonne 1) ...
    cout << "\x1b[31;1m" << "Hello"; (1)
    // ... suivi par "world !" en jaune sur fond bleu et souligné
    cout << "\x1b[33;44;4m" << " world !";
    // Réinitialise sur les couleurs par défaut
    // et désactive le soulignement
    cout << "\x1b[39;49;24m" << endl;

    return 0;
}
1 On peut aussi écrire cout << "\x1b[31;1mHello"; mais c’est moins lisible

L’exécution de ce programme affichera :

vt100

Mise en situation

On désire faire une application qui affiche toutes les secondes la mémoire libre et la charge moyenne d’une machine Linux (voir Load average link ).

vt100 dashboard

Travail à faire

Cette activité est réalisable sur tout environnement Linux (PC ou Raspberry Pi) mais également à l’aide d’un compilateur C/C++ en ligne comme GDB online Debugger link

🖮 Travail n° 1 : Amélioration de l’IHM

On vous donne ci-dessous le code partiel de cette application :

Code source
vt100-dashboard.cpp
/**
 * Affiche un tableau de bord qui affiche en permanence la mémoire libre
 * disponible et la charge moyenne du système.
 *
 * Compilation : g++ -o vt100-dashboard vt100-dashboard.cpp
 * Exécution : ./vt100-dashboard
 *
 */
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <string>
#include <iostream>
#include <thread>

using namespace std;

/**
 * Désactive le mode canonique du terminal
 * (i.e. interprétation par caractère plutôt que par ligne)
 */
void disable_canonical_mode()
{
    termios term;
    tcgetattr(0, &term);
    term.c_lflag &= ~(ICANON | ECHO);
    tcsetattr(0, TCSANOW, &term);
}

/**
 * Active le mode canonique du termianl
 * (i.e. interprétation par ligne plutôt que par caractère)
 */
void enable_canonical_mode()
{
    termios term;
    tcgetattr(0, &term);
    term.c_lflag |= ICANON | ECHO;
    tcsetattr(0, TCSANOW, &term);
}

/**
 * Indique si une touche clavier a été enfoncée
 */
bool kbhit()
{
    int byteswaiting;
    ioctl(0, FIONREAD, &byteswaiting);
    return byteswaiting > 0;
}


/**
 * Récupère la sortie d'une commande
 */
string getStdoutFromCommand(string cmd) {
  string data;
  FILE * stream;
  const int max_buffer = 256;
  char buffer[max_buffer];
  cmd.append(" 2>&1");

  stream = popen(cmd.c_str(), "r");

  if (stream) {
    while (!feof(stream))
      if (fgets(buffer, max_buffer, stream) != NULL) data.append(buffer);
    pclose(stream);
  }
  return data;
}

/**
 * Retourne la mémoire libre en octets
 */
unsigned long  getFreeMem() {
    string freeMemStr = getStdoutFromCommand("cat /proc/meminfo | sed -n '2p' | grep -P '\\d+' -o");
    return stoi(freeMemStr);
}

/**
 * Retourne l'heure avec le format hh:mm:ss
 */
string getHour() {
    return getStdoutFromCommand("date '+\%T'");
}

/**
 * Retourne la charge moyenne du processeur
 * Voir https://fr.wikipedia.org/wiki/Load_average
 */
float getLoadAvg() {
   string loadAvgStr =  getStdoutFromCommand("uptime | grep -P '[\\d.]+$' -o");
   return stof(loadAvgStr);
}

/**
 * Point d'entrée du programme
 */
int main()
{
    disable_canonical_mode();

    cout << "\x1b[2J";

    cout << "\x1b[1;1H" << "Heure          :" << endl;
    cout << "\x1b[2;1H" << "Mémoire libre  :" << endl;
    cout << "\x1b[4;1H\x1b[36m" << "<Appuyer sur une touche pour quitter>" << "\x1b[39m" << endl;

    // Mettre à jour les informations toutes les secondes tant qu'une
    // touche n'est pas appuyée
    while( !kbhit() ) {
        cout << "\x1b[1;18H" << getHour();
        cout << "\x1b[2;18H" << getFreeMem() << " kB";
        this_thread::sleep_for(1000ms);
    }

    enable_canonical_mode();
    tcflush(STDIN_FILENO, TCIFLUSH);

    cout << "\x1b[4;1H\x1b[2K" << "Bye !" << endl;

    return 0;
}
  1. Analyser le code source fourni et le compléter au niveau de la fonction main() pour réaliser l’affichage de la charge moyenne du système en dessous de celui de la mémoire libre en utilisant la séquence de contrôle VT100 qui permet de contrôler la position du curseur.

    La charge moyenne du système — déterminée durant les 15 dernières minutes — est retournée par la fonction getLoadAvg() présente dans le code source fourni.

  2. À partir des documents fournis en référence (Console Virtual Terminal Sequences link, VT100 commands and control sequences link), déterminer les séquences de contrôle permettant d’activer/désactiver l’affichage du curseur puis compléter le code pour faire disparaître le curseur pendant toute la durée d’exécution du programme.

    Ne pas oublier de réactiver l’affichage curseur en fin de programme

  3. Modifier l’affichage pour que chaque information (heure, mémoire, charge) se fasse désormais en gras (→ bold/bright)

  4. Faire en sorte que l’heure soit aussi soulignée

  5. Faire évoluer le code pour afficher en rouge la charge système lorsqu’elle dépasse un certain seuil d’alerte et en vert lorsqu’elle est en deçà.

    • Le seuil d’alerte sera programmé “en dur” dans un 1er temps

    • Vous pouvez provoquer une augmentation de charge du système en invoquant, depuis un terminal, plusieurs fois la commande yes > /dev/null &.

      Taper ensuite sudo killall yes pour revenir à la normale.

  6. Faire évoluer le code pour quitter l’application lorsque l’utilisateur appuie sur la touche q ou Q

    Pour lire la valeur de la touche appuyée qui a fait quitter la boucle controlée par kbhit() utiliser :

    int c = getchar();
  7. Faire évoluer le code pour afficher la mémoire libre cycliquement en kilo-octets, méga-octets, giga-octets lorsque l’utilisateur appuie sur la touche u (pour “unité”)

🖮 Travail n° 2 : Arguments de la ligne de commande

On désire à présent pouvoir fournir le seuil d’avertissement de dépassement de charge de système à partir d’un argument depuis la ligne de commande. Ex. :

./vt100-dashboard -m 0.25 (1)
1 On précise un seuil de 0.25 grâce à l’option -m
  1. Se renseigner sur internet sur les fonctions getopt() et/ou getopt_long() de façon à ajouter cette nouvelle amélioration à votre programme.

Proposition de corrigé

Une proposition de corrigé pour le programme final est fournie ci-après dans une archive compressée et protégée par un mot de passe. Le demander à l’enseignant pour avoir accès à son contenu.

Illustration de l’exécution sur GDB online Debugger link :

vt100 dashboard gdbonline

🞄  🞄  🞄