Configuration d’une application embarquée (2/2)

Publié par cpb
Avr 29 2013

Configuration Application EmbarquéCet article a été écrit en collaboration avec mon ami François Beaulier (dont je vous recommande le blog). Nous avons réfléchi ensemble sur les possibilités qui s’offrent au développeur d’application embarquée pour enregistrer le paramétrage, la configuration, les préférences… bref tout ce qui constitue les données persistantes de son système.

Nous avons examiné dans la première partie de cet article plusieurs méthodes de stockage des données persistantes (configuration, préférences, etc.) d’une application embarquée : sauvegarde binaire “brute”, formatage ASCII avec fprintf(), fichier de type .ini à la manière de Windows. Aucune de ces méthodes ne nous donnent entièrement satisfaction dès lors qu’il nous faut garantir une certaine portabilité entre plate-formes, et que nos données peuvent être organisées en tableaux ou en structure arborescente.

Dans cette seconde partie nous allons aborder des solutions plus évoluées qui permettent de répondre beaucoup mieux au besoin.

Format spécifique libconfig

Il est intéressant d’étudier la solution des bibliothèques open source dédiées à ce problème des fichiers de configuration. Une des plus connues est libconfig qui propose un format de fichier spécifique de type texte avec un parser doté d’une API simple et efficace.

Voila ce que dit la documentation sur l’objectif du projet :

------------------- >
1.1 Why Another Configuration File Library?

There are several open-source configuration file libraries available as of this writing. This library was written because each of those libraries falls short in one or more ways. The main features of libconfig that set it apart from the other libraries are:

    A fully reentrant parser. Independent configurations can be parsed in concurrent threads at the same time.

    Both C and C++ bindings, as well as hooks to allow for the creation of wrappers in other languages.

    A simple, structured configuration file format that is more readable and compact than XML and more flexible than the obsolete but prevalent Windows “INI” file format.

    A low-footprint implementation (just 37K for the C library and 76K for the C++ library) that is suitable for memory-constrained systems.

    Proper documentation.     

<-------------------

La prise en main de la bibliothèque est rapide et le format de fichier assez intuitif. La bibliothèque gère directement les types de données de base : integer, integer64, double, boolean et string. On peut définir des groupes nommés, comme les sections du .ini, sauf que l’on peut les imbriquer. Il y a également une syntaxe pour déclarer des tableaux et des listes.

Les tableaux doivent contenir des types de base, on ne peut donc pas faire de tableaux d’objets comme des structures mais par contre les listes permettent de le faire.

Voici une manière de représenter les données de notre application de domotique, en intégrant l’aspect relatif à la composition : les objets capteurs et actionneurs sont définis comme des listes à l’intérieur de chaque pièce.

# Demo application domotique

version = "1.0";

site:
{
  Nom = "Restaurant le Marco Polo";
  Adresse = "4 avenue d'Italie 75013 Paris";
  Pieces = (
    {
      Nom = "Cuisine";
      Volume = 92.4;
      Capteurs = (
        {
          Type = "PT100";
          Identifiant = 100;
        },
        {
          Type = "KZ08";
          Identifiant = 101;
        }
      )
      Actionneurs = (
        {
          Type = "REL01";
          Identifiant = 200;
        },
        {
          Type = "REL01";
          Identifiant = 201;
        }
      )
    },
    {
      Nom = "Salle";
      Volume = 148.3;
      Capteurs = (
        {
          Type = "PT100";
          Identifiant = 102;
        },
        {
          Type = "AS12";
          Identifiant = 103;
        },
        {
          Type = "AS12";
          Identifiant = 104;
        }
      )
      Actionneurs = (
        {
          Type = "REL01";
          Identifiant = 202;
        },
        {
          Type = "REL01";
          Identifiant = 203;
        }
      )
    }
  )
}

Bien sûr comme nous l’avions précisé avec le format .ini ce n’est pas la seule manière de faire, les liens de composition peuvent être enregistrés explicitement avec des champs supplémentaires dans les objets.

Voici le code source permettant de remplir nos structures à partir de ce fichier de configuration. La définition des structures a été un peu revue pour avoir des chaînes de caractères allouées dynamiquement.

/*************************************************************************
** Demo utilisation de Libconfig
*************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include "../libconfig-1.4.9/lib/libconfig.h"

#define MAX_CAPTEURS_PAR_PIECE     10
#define MAX_ACTIONNEURS_PAR_PIECE  10

#define MAX_PIECES                 10

enum capteur_t {CAPT_UNDEFINED = 0, CAPT_PT100, CAPT_KZ08, CAPT_AS12, CAPT_LAST };
enum actionneur_t {ACT_UNDEFINED = 0, ACT_REL01, ACT_LAST };

char *capt_names[] = {"UNDEFINED", "PT100", "KZ08", "AS12"};
char *act_names[] = {"UNDEFINED", "REL01"};

struct s_capteur {
   enum capteur_t type;
   int identifiant;
};

struct s_actionneur {
   enum actionneur_t type;
   int identifiant;
};

struct s_piece{
   char *nom;
   double volume;
   struct s_capteur tabCapteurs[MAX_CAPTEURS_PAR_PIECE ];
   struct s_actionneur tabActionneurs[MAX_ACTIONNEURS_PAR_PIECE ];
};

struct s_site {
   char *nom;
   char *adresse;
   struct s_piece tabPieces[MAX_PIECES];
};

struct s_site site_config;
struct config_t cfg;

int main(void)
{
   const char *s = NULL;
   char *version = NULL;
   int name;

   /* Initialize the configuration */
   config_init(& cfg);

   /* Load the file */
   printf("loading domotic.cfg ..");
   if (!config_read_file(& cfg, "domotic.cfg")){
     printf("failed : %s\n", config_error_text(& cfg));
     exit(1);
   }
   config_setting_t *cfs = NULL;
   printf(" ok\n");

   /* Get the first item at root */
   if(config_lookup_string(& cfg, "version", & s) == CONFIG_TRUE)
       version = strdup(s);
   if(version)
       printf("version = %s\n", version);

   /* Get the site group */
   cfs = config_lookup(& cfg, "site");
   if(!cfs){
       printf("error\n");
       exit(1);
   }

   /* Get name and address in site */
   if(config_setting_lookup_string(cfs, "Nom", & s) == CONFIG_TRUE)
       site_config.nom = strdup(s);
   if(config_setting_lookup_string(cfs, "Adresse", & s) == CONFIG_TRUE)
       site_config.adresse = strdup(s);

   /* Parse all pieces */
   config_setting_t *list_pieces = config_setting_get_member(cfs, "Pieces");
   int piece = 0;
   int capt = 0;
   int act = 0;
   while(list_pieces) {
       struct s_piece *pPiece = & site_config.tabPieces[piece];
       config_setting_t *pcs = config_setting_get_elem(list_pieces, piece);
       if(!pcs)
           break;
       if(config_setting_lookup_string(pcs, "Nom", & s) == CONFIG_TRUE)
           pPiece->nom = strdup(s);
       config_setting_lookup_float(pcs, "Volume", & pPiece->volume);
       /* Parse all capteurs */
       config_setting_t *list_capteurs = config_setting_get_member(pcs, "Capteurs");
       capt = 0;
       while(list_capteurs){
           config_setting_t *cpt = config_setting_get_elem(list_capteurs, capt);
           if(!cpt)
               break;
           if(config_setting_lookup_string(cpt, "Type", & s) == CONFIG_FALSE)
               continue;
           for(name = 0; name < CAPT_LAST; name++)
               if(!strcmp(s, capt_names[name]))
                   pPiece->tabCapteurs[capt].type = name;
           config_setting_lookup_int(cpt, "Identifiant", & pPiece->tabCapteurs[capt].identifiant);
           capt++;
           if(capt >= MAX_CAPTEURS_PAR_PIECE)
               break;
       }
       /* Parse all actionneurs */
       config_setting_t *list_actionneurs = config_setting_get_member(pcs, "Actionneurs");
       act = 0;
       while(list_actionneurs){
           config_setting_t *ant = config_setting_get_elem(list_actionneurs, act);
           if(!ant)
               break;
           if(config_setting_lookup_string(ant, "Type", &s) == CONFIG_FALSE)
               continue;
           for(name = 0; name < ACT_LAST; name++)
               if(!strcmp(s, act_names[name]))
                   pPiece->tabActionneurs[act].type = name;
           config_setting_lookup_int(ant, "Identifiant", &pPiece->tabActionneurs[act].identifiant);
           act++;
           if(act >= MAX_ACTIONNEURS_PAR_PIECE)
               break;
       }
       piece++;
       if(piece >= MAX_PIECES)
           break;
   }
   /* Free the configuration */
   config_destroy(&cfg);
   /* Display in-application configuration */
   printf("Site : %s %s\n", site_config.nom, site_config.adresse);
   for(piece = 0 ; piece < MAX_PIECES ; piece++){
       struct s_piece *pPiece = &site_config.tabPieces[piece];
       if(!pPiece->nom)
           break;
       printf("\n------------piece #%d -------------\n", piece);
       printf("%s : volume = %g\n", pPiece->nom, pPiece->volume);
       for(capt = 0 ; capt > MAX_CAPTEURS_PAR_PIECE; capt++){
           if(!pPiece->tabCapteurs[capt].type)
               break;
           printf("Capteur type %s identifiant = %d\n", capt_names[pPiece->tabCapteurs[capt].type],
                                                         pPiece->tabCapteurs[capt].identifiant);
       }
       for(act = 0 ; act < MAX_ACTIONNEURS_PAR_PIECE; act++){
           if(!pPiece->tabActionneurs[act].type)
               break;
           printf("Actionneur type %s identifiant = %d\n", act_names[pPiece->tabActionneurs[act].type],
                                                           pPiece->tabActionneurs[act].identifiant);
       }
   }
   return 0;
}

Après la suppression de l’instance de la configuration on affiche le contenu des structures afin de vérifier que tout est en place et que les objets dynamiques ont bien été recopiés.

Au niveau de la partie parsing proprement dite on s’en sort assez simplement avec deux boucles imbriquées, l’API de la bibliothèque nous permet de parcourir les listes avec un index et donc de remplir directement nos tableaux de structures. Le fait d’avoir un jeu de fonctions pour lire chaque type de donnée directement est bien pratique.

Nous avons là une solution assez élégante, le fichier reste modifiable manuellement tant que le nombre de niveaux d’imbrication reste faible. D’autre part libconfig est un projet très mature dans lequel on peut avoir une assez bonne confiance.

Si vous voulez compiler ce code attention à la version de libconfig disponible sur les distributions. Dans mon cas la syntaxe utilisée n’était pas supportée, j’ai recompilé la dernière version proposée sur le site. Inutile d’installer la bibliothèque et de risquer de perturber les dépendances de votre distribution, faites juste une compilation puis utilisez la bibliothèque statique libconfig.a en l’ajoutant à la ligne de commande de gcc :

gcc -O2 -Wall -o domo domo.c ../libconfig-1.4.9/lib/.libs/libconfig.a

Et plutot que d’inclure un <libconfig.h> qui risque d’être incompatible, utilisez un chemin en dur :

  #include "../libconfig-1.4.9/lib/libconfig.h"

Pour être certain d’avoir le bon.

Format structuré arborescent Xml

Le Xml est une solution que j’ai utilisée avec succès pendant des années sur des projets Linux embarqué. Au départ mon idée était simplement de trouver “quelque chose de standard” pour stocker ma configuration. J’espérais également pouvoir profiter de la disponibilité d’outils existants pour éditer et vérifier mes fichiers.

Le Xml est assez déroutant au début car tout ce qui tourne autour de cette technologie est très abstrait et pas forcément attirant pour les programmeurs du monde embarqué. Pourtant si l’on se contente des principes de base on peut avoir facilement quelque chose de très efficace.

La lecture de fichiers Xml est quelque chose d’hyper-standard et la plupart des langages ont une API native pour le faire. Il existe deux types d’API pour accéder au Xml, qui correspondent a deux principes fondamentaux différent pour le parsing du fichier:

  • le SAX (simple API for Xml) repose sur un principe asynchrone d’appel de handlers. Le parser parcourt tout le fichier Xml et appelle les handlers que l’on a déclarés pour chaque balise rencontrée. Cette API est particulièrement adaptée à la lecture de documents volumineux car elle évite de charger tout le contenu en mémoire.
  • le DOM (Document Object Model) en revanche charge l’intégralité du document sous la forme d’un arbre en mémoire. Ensuite des fonctions permettent de parcourir, rechercher, lire, écrire et créer des nœuds.

Bien que ces deux principes soient des standards il n’existe pas à ma connaissance de standard sur l’API elle même et chaque implémentation aura ses particularités.

Une bibliothèque très bien faite, simple et de petite taille, pour la lecture et l’écriture de fichiers Xml depuis une application est Mini-Xml. Elle est en général disponible sur les distributions sous le nom de libmxml, sachant que pour l’utiliser en développement il faudra installer le paquet libmxml-dev. L’API proposée est de type DOM et de nombreuses fonctions permettent de rechercher ou parcourir l’arbre.

Voyons à quoi peut ressembler notre fichier de configuration:

<?xml version = '1.0' encoding = 'UTF-8'?>
<Root_Element>
  <Version>1.0</Version>
  <Site>
    <Nom>Restaurant le Marco Polo</Nom>
    <Adresse>4 avenue d'Italie 75013 Paris</Adresse>
    <Piece>
      <Nom>Cuisine</Nom>
      <Volume>92.4</Volume>
      <Capteur>
        <Type>PT100</Type>
        <Identifiant>100</Identifiant>
      </Capteur>
      <Capteur>
        <Type>KZ08</Type>
        <Identifiant>101</Identifiant>
      </Capteur>
      <Actionneur>
        <Type>REL01</Type>
        <Identifiant>200</Identifiant>
      </Actionneur>
      <Actionneur>
        <Type>REL01</Type>
        <Identifiant>201</Identifiant>
      </Actionneur>
    </Piece>
    <Piece>
      <Nom>Salle</Nom>
      <Volume>148.3</Volume>
      <Capteur>
        <Type>PT100</Type>
        <Identifiant>102</Identifiant>
      </Capteur>
      <Capteur>
        <Type>AS12</Type>
        <Identifiant>103</Identifiant>
      </Capteur>
      <Capteur>
        <Type>AS12</Type>
        <Identifiant>104</Identifiant>
      </Capteur>
      <Actionneur>
        <Type>REL01</Type>
        <Identifiant>202</Identifiant>
      </Actionneur>
      <Actionneur>
        <Type>REL01</Type>
        <Identifiant>203</Identifiant>
      </Actionneur>
    </Piece>
  </Site>
</Root_Element>

Effectivement par rapport à libconfig la syntaxe est plus bavarde puisque chaque nom de balise est répété deux fois. Ceci dit la structure m’apparaît plus claire et plus lisible, on identifie très bien les différentes branches et une modification manuelle est facilement faisable.

Premier avantage du Xml, si je souhaite vérifier que mon fichier est bien formé au sens Xml, c’est à dire que je n’ai pas oublié une balise fermante par exemple, il existe des outils tout prêts. Par exemple rxp est un petit outil de validation en ligne de commande :

$ rxp -s config.xml

Si aucun message d’erreur, mon fichier est correct. Enlevons par exemple la dernière balise </Actionneur>.

$ rxp -s config.xml
Error: Mismatched end tag: expected </Actionneur>, got </Piece>
in unnamed entity at line 49 char 10 of file:///home/francois/domotic-xml/config.xml

Et voila l’erreur signalée !

On pourrait également écrire un fichier de document type définition DTD qui permettrais à rxp de vérifier beaucoup plus finement le contenu du fichier.

Pour la lecture du fichier le tout est de bien comprendre les fonctions de l’API de Mini-Xml. Il y a un petit tutoriel très utile sur le site. Ensuite il reste un coté un peu fastidieux par le fait de tester le nom de chaque balise et de faire la conversion qui s’impose vers le bon type de variable. C’est la que libconfig offre un avantage par le fait de proposer des fonction de conversions.

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <mxml.h>

#define MAX_CAPTEURS_PAR_PIECE 10
#define MAX_ACTIONNEURS_PAR_PIECE 10
#define MAX_PIECES 10

enum capteur_t {CAPT_UNDEFINED = 0, CAPT_PT100, CAPT_KZ08, CAPT_AS12, CAPT_LAST };
enum actionneur_t {ACT_UNDEFINED = 0, ACT_REL01, ACT_LAST };

char *capt_names[] = {"UNDEFINED", "PT100", "KZ08", "AS12"};
char *act_names[] = {"UNDEFINED", "REL01"};

struct s_capteur {
   enum capteur_t type;
   int identifiant;
};

struct s_actionneur {
   enum actionneur_t type;
   int identifiant;
};

struct s_piece{
   char *nom;
   double volume;
   struct s_capteur tabCapteurs[MAX_CAPTEURS_PAR_PIECE ];
   struct s_actionneur tabActionneurs[MAX_ACTIONNEURS_PAR_PIECE ];
};

struct s_site {
   char *nom;
   char *adresse;
   struct s_piece tabPieces[MAX_PIECES];
};

struct s_site site_config = {0};
char *version = NULL;

/*
 * Utilise mini-xml pour charger dans un arbre en memoire le contenu du fichier FileName
 * FileName : chemin du fichier à ouvrir
 * return : Pointeur sur le noeud racine de l'arbre
 */
mxml_node_t *loadXmlTree(const char *FileName)
{
   FILE *fp;
   mxml_node_t *tree;

   fp = fopen(FileName, "r");
   if(fp == NULL) {
      printf("Erreur ouverture en lecture %s :\n",FileName);
      perror("fopen");
      return NULL;
   }
   tree = mxmlLoadFile(NULL, fp, MXML_OPAQUE_CALLBACK);
   if(tree == NULL)
      printf("Erreur lecture xml %s\n",FileName);
   fclose(fp);
   return tree;
}

void parseActionneur(mxml_node_t *node, struct s_actionneur *pAct)
{
   char *SubValue, *SubTag = NULL;
   mxml_node_t *fils=node->child;
   int name;

   while(fils != NULL) {
      if ((fils->type != MXML_ELEMENT)
       ||(fils->child == NULL)) {
         fils = fils->next;
         continue;
      }
      SubValue = fils->child->value.opaque;
      SubTag=fils->value.element.name;
      if (strcmp (SubTag, "Type") == 0){
         for(name = 0; name < ACT_LAST; name++)
         if(!strcmp(SubValue, act_names[name]))
            pAct->type = name;
      } else if (strcmp (SubTag, "Identifiant") == 0) {
         pAct->identifiant = strtol(SubValue, NULL, 10);
      }
      fils=fils->next;
   }
}

void parseCapteur(mxml_node_t *node, struct s_capteur *pCapt)
{
   char *SubValue, *SubTag=NULL;
   mxml_node_t *fils=node->child;
   int name;

   while(fils != NULL) {
      if ((fils->type != MXML_ELEMENT)
       ||(fils->child == NULL)) {
         fils = fils->next;
         continue;
      }
      SubValue=fils->child->value.opaque;
      SubTag=fils->value.element.name;

      if (strcmp (SubTag, "Type") == 0){
         for(name = 0; name < CAPT_LAST; name++)
         if (! strcmp(SubValue, capt_names[name]))
            pCapt->type = name;

      } else if (strcmp (SubTag, "Identifiant") == 0) {
         pCapt->identifiant = strtol(SubValue, NULL, 10);
      }
      fils=fils->next;
   }
}

void parsePiece(mxml_node_t *node, struct s_piece *pPiece)
{
   char *SubValue, *SubTag=NULL;
   mxml_node_t *fils=node->child;
   int IdxCapt = 0;
   int IdxAct = 0;
   while(fils != NULL) {
      if ((fils->type != MXML_ELEMENT)
       ||(fils->child == NULL)) {
         fils=fils->next;
         continue;
      }
      SubValue=fils->child->value.opaque;
      SubTag=fils->value.element.name;
      if (strcmp (SubTag, "Nom") == 0)
         pPiece->nom = strdup(SubValue);
      else if (strcmp (SubTag, "Volume") == 0)
         pPiece->volume = strtod(SubValue, NULL);
      else if (strcmp (SubTag, "Capteur") == 0)
         parseCapteur(fils, &pPiece->tabCapteurs[IdxCapt++]);
      else if (strcmp (SubTag, "Actionneur") == 0)
         parseActionneur(fils, &pPiece->tabActionneurs[IdxAct++]);
      fils=fils->next;
   }
}

void parseSite(mxml_node_t *node, struct s_site *pSite)
{
   char *SubValue, *SubTag=NULL;
   mxml_node_t *fils=node->child;
   int IdxPiece = 0;

   while(fils != NULL) {
      if ((fils->type != MXML_ELEMENT)
       ||(fils->child == NULL)) {
         fils = fils->next;
         continue;
      }

      SubValue=fils->child->value.opaque;
      SubTag=fils->value.element.name;

      if (strcmp (SubTag, "Nom") == 0)
         site_config.nom = strdup(SubValue);
      else if (strcmp (SubTag, "Adresse") == 0)
         site_config.adresse = strdup(SubValue);
      else if (strcmp (SubTag, "Piece") == 0)
         parsePiece(fils, &pSite->tabPieces[IdxPiece++]);
      fils=fils->next;
   }
}

void parseConfig(mxml_node_t *tree, struct s_site *pSite)
{
   char *SubValue, *SubTag=NULL;

   mxml_node_t *node = mxmlFindElement(tree, tree, "Root_Element", NULL, NULL, MXML_DESCEND);
   mxml_node_t *fils = node->child;

   while (fils != NULL) {
      if ((fils->type != MXML_ELEMENT)
       ||(fils->child == NULL)) {
         fils = fils->next;
         continue;
      }

      SubValue=fils->child->value.opaque;
      SubTag=fils->value.element.name;
      if(strcmp(SubTag,"Version") == 0)
         version = strdup(SubValue);
      else if(strcmp(SubTag,"Site") == 0)
         parseSite(fils, pSite);
      fils=fils->next;
   }
}

int main(void)
{
   int piece, capt, act;
   mxml_node_t *tree = loadXmlTree("domotic.xml");

   if(tree == NULL)
      return -1;

   parseConfig(tree, &site_config);
   mxmlDelete(tree);

   /* Display in-application configuration */
   printf("Site : %s %s\n", site_config.nom, site_config.adresse);
   for(piece = 0 ; piece < MAX_PIECES ; piece++){
      struct s_piece *pPiece = &site_config.tabPieces[piece];
      if(!pPiece->nom)
         break;
      printf("\n------------piece #%d -------------\n", piece);
      printf("%s : volume = %g\n", pPiece->nom, pPiece->volume);
      for(capt = 0 ; capt < MAX_CAPTEURS_PAR_PIECE; capt++){
         if(! pPiece->tabCapteurs[capt].type)
            break;
         printf("Capteur type %s identifiant = %d\n", capt_names[pPiece->tabCapteurs[capt].type],
         pPiece->tabCapteurs[capt].identifiant);
      }
      for(act = 0 ; act < MAX_ACTIONNEURS_PAR_PIECE; act++){
         if(! pPiece->tabActionneurs[act].type)
            break;
         printf("Actionneur type %s identifiant = %d\n", act_names[pPiece->tabActionneurs[act].type],
         pPiece->tabActionneurs[act].identifiant);
      }
   }
   return 0;
}

Base de données

L’emploi d’une base de données peut paraître a priori démesuré pour enregistrer la configuration d’un système embarqué car cela évoque immédiatement la notion de serveur à l’administration assez lourde (PostgreSQL, MySQL/MariaDB, Oracle,…). Il existe toutefois des implémentations très simples, prévues pour l’embarqué. La plus répandue dans ce domaine est SQLite, que nous allons examiner ici.

SQLite est en réalité une bibliothèque capable de gérer directement les fichiers de la base de données sans passer par un serveur distinct. Lorsque nous lions notre application avec cette bibliothèque, cela lui permet d’utiliser la base de données de manière autonome. Ce mécanisme est naturellement limité en terme de performances, surtout lors d’accès en parallèle depuis plusieurs applications, mais le but de SQLite n’est pas d’être une alternative aux SGBD classiques gérant des bases volumineuses, mais plutôt de représenter une solution portable, élégante et facile à maintenir pour enregistrer des éléments de configuration.

Accès depuis le shell

Il existe un petit utilitaire nommé sqlite3 qui accepte des requêtes SQL sur son entrée standard (ou sa ligne de commande) et les applique à la base de données indiquée en argument. Sans redirection de son entrée-standard, il présente une interface interactive simple. Voici un exemple dans lequel nous allons utiliser un script shell qui créera une base de données. Pour cela nous employons une syntaxe avec un “Here Document” comme c’est souvent l’usage.

creation-bdd.sh:
#! /bin/sh

BDD=./domotique.sql

rm -f "${BDD}" || { echo "impossible d'effacer ${BDD}."; exit 1; }

touch "${BDD}" || { echo "impossible de creer ${BDD}.";  exit 1; }

sqlite3 "${BDD}" << FIN_SITE
       CREATE TABLE site (
           nom     CHAR(64),
           adresse CHAR(512),
           ref     INTEGER PRIMARY KEY
       );
FIN_SITE

sqlite3 "${BDD}" << FIN_PIECE
       CREATE TABLE piece (
           nom      CHAR(256),
           volume   FLOAT,
           ref_site INTEGER,
           ref      INTEGER PRIMARY KEY,
           FOREIGN KEY (ref_site) REFERENCES site(ref)
       );
FIN_PIECE

sqlite3 "${BDD}" << FIN_CAPTEUR
       CREATE TABLE capteur (
           type        CHAR(32),
           identifiant CHAR(32),
           ref_piece   INTEGER,
           FOREIGN KEY(ref_piece) REFERENCES piece(ref)
           PRIMARY KEY(ref_piece, identifiant)
       );
FIN_CAPTEUR

sqlite3 "${BDD}" << FIN_ACTIONNEUR
       CREATE TABLE actionneur (
           type        CHAR(32),
           identifiant CHAR(32),
           ref_piece   INTEGER,
           FOREIGN KEY(ref_piece) REFERENCES piece(ref)
           PRIMARY KEY(ref_piece, identifiant)
       );
FIN_ACTIONNEUR

Ce script commence par effacer l’éventuelle base de données précédente avant de la reconstruire (sous forme d’un unique fichier domotique.sql) en y créant quatre tables : une pour le(s) site(s) supervisé(s), une pour les pièces se trouvant dans ce site, et deux pour les capteurs et actionneurs disponibles dans chaque pièce. La structure des enregistrements est encore arborescente, car chaque pièce dispose d’un champ “ref_site” indiquant le numéro de référence du chantier auquel elle appartient. De même les capteurs et actionneurs ont des champs “ref_piece“ pour connaître leurs emplacements. Ce script a un comportement plutôt strict car il efface toutes les données précédentes. On peut l’assimiler à une fonctionnalité “Retour aux paramètres d’usine” présente sur la plupart des systèmes embarqués.

La construction des tables est un peu compliquée par l’usage des contraintes FOREIGN KEY et PRIMARY KEY. L’objectif est de garantir l’unicité de chaque site par son numéro de référence, puis de chaque pièce (par son numéro de référence également) et enfin de chaque capteur en combinant numéro de pièce et identifiant de capteur – et de chaque actionneur suivant le même principe. La notion de FOREIGN KEY permet de s’assurer que le champ ref_site d’une pièce corresponde bien avec un site existant dans la base (et de même pour les champs ref_piece des capteurs et actionneurs).

Attention : par défaut SQLite3 n’effectue pas de vérification de cohérence sur les clés FOREIGN. Pour activer cette vérification il faudra ajouter une directive « PRAGMA foreign_keys=ON” lors de l’insertion de nouveaux enregistrements.

Nous pouvons vérifier la composition de nos tables directement depuis le shell en utilisant la commande “.schema” de sqlite3.

$ sqlite3 domotique.sql .schema
CREATE TABLE actionneur (
       type        CHAR(32),
       identifiant CHAR(32),
       ref_piece   INTEGER,
       FOREIGN KEY(ref_piece) REFERENCES piece(ref)
       PRIMARY KEY(ref_piece, identifiant)
   );

CREATE TABLE capteur (
       type        CHAR(32),
       identifiant CHAR(32),
       ref_piece   INTEGER,
       FOREIGN KEY(ref_piece) REFERENCES piece(ref)
       PRIMARY KEY(ref_piece, identifiant)
   );

CREATE TABLE piece (
       nom         CHAR(256),
       volume   FLOAT,
       ref_site INTEGER,
       ref      INTEGER PRIMARY KEY,
       FOREIGN KEY (ref_site) REFERENCES site(ref)
   );

CREATE TABLE site (
       nom     CHAR(64),
       adresse CHAR(512),
       ref     INTEGER PRIMARY KEY
   );
$

Nous allons commencer à remplir nos tables. Pour créer un premier site avec deux pièces je vais utiliser à nouveau un petit script shell. On pourrait imaginer que ce script soit téléchargé sur les systèmes embarqués lors d’une mise à jour, en transférant un nouveau firmware par exemple.

initialisation-table.sh:
#! /bin/sh

BDD=./domotique.sql

sqlite3 "${BDD}" << FIN_INITIALISATION
   PRAGMA foreign_keys=ON;
   INSERT INTO site VALUES    (
       "Restaurant le Marco Polo",
       "4, avenue d'Italie - 75013 Paris",
       1
   );

   INSERT INTO piece (nom, volume, ref_site) VALUES (
       "Cuisine",
        92.4,
        1
   );

   INSERT INTO piece (nom, volume, ref_site) VALUES (
       "Salle",
       148.3,
       1
   );
FIN_INITIALISATION

Pour l’enregistrement du site, nous avons rempli tous les champs, y compris le numéro de référence, car il est utilisé explicitement lors de la création des pièces. Sachez que SQLite peut parfaitement fournir des valeurs entières uniques lorsque la clé primaire n’est pas indiquée. C’est ce qui se passe pour l’enregistrement des deux pièces où nous n’avons pas fourni de valeurs pour le champ “ref” (nous verrons les valeurs attribuées un peu plus loin).

Pour la saisie des capteurs, nous allons changer de langage. Je vais utiliser un script PHP pour créer les capteurs. Ceci est assez représentatif d’un système embarqué sur lequel le paramétrage est réalisé à partir d’une petite interface HTML (avec un serveur HTTP embarqué comme Lighttpd).

Voici un script PHP qui attend sur sa ligne de commande le nom du site et celui de la pièce concernée, puis il ajoute un capteur du type indiqué avec le numéro de référence correspondant (bien entendu s’il était appelé par un serveur HTTP, les paramètres seraient dans $_GET[ ] ou $POST[ ] par exemple).

insertion-capteur.php:
<?php
       if ($argc != 5) {
               die("usage: $argv[0] <nom_site> &lt,nom_piece> <type_capteur> <identifiant_capteur>\n");
       }

       $nom_site  = "$argv[1]";
       $nom_piece = "$argv[2]";
       $type_capteur = "$argv[3]";
       $identifiant_capteur = "$argv[4]";

       $db = new SQLite3("domotique.sql");
       $result = $db->query('SELECT * FROM site WHERE nom="'.$nom_site.'";');
       if ($result == null) {
               die("Erreur dans l'accès à la base de données\n");
       }

       $line = $result->fetchArray();
       if ($line == null) {
               die("Impossible de trouver le site : $nom_site\n");
       }

       $ref_site = $line['ref'];
       $result = $db->query('SELECT * FROM piece WHERE nom="'.$nom_piece.'" AND ref_site="'.$ref_site.'";');
       if ($result == null) {
               die("Erreur dans l'accès à la base de données\n");
       }

       $line = $result->fetchArray();
       if ($line == null) {
               die("Impossible de trouver la piece : $nom_piece\n");
       }

       $ref_piece = $line['ref'];
       $db->query("INSERT INTO capteur VALUES ('".$type_capteur."','".$identifiant_capteur."','".$ref_piece."')");
?>

À noter : nous avons utilisé ici le module SQLite3 de PHP. Nous pourrions très bien accéder à notre base par le module PDO (PHP Data Object) qui est une interface d’abstraction de plus haut niveau. J’ai une préférence pour SQLite3 dans le contexte embarqué car l’API est plus simple, en outre elle nous permet d’ouvrir une base de données en lecture-seule (option très utile sur les systèmes embarqués où l’alimentation électrique peut être coupée intempestivement), ce qui n’est à ma connaissance pas possible avec PDO.

Insérons deux capteurs dans la cuisine de notre restaurant :

$ php insertion-capteur.php "Restaurant le Marco Polo" "Cuisine" "PT100" "100"
$ php insertion-capteur.php "Restaurant le Marco Polo" "Cuisine" "KZ08" "101"

Nous pouvons appeler sqlite3 directement pour vérifier l’insertion des capteurs :

$ sqlite3 domotique.sql "SELECT * FROM capteur;"
PT100|100|1
KZ08|101|1
$

La bibliothèque SQLite3 est évidemment accessible depuis le langage C par exemple. Voici un petit exemple de programme qui va consulter et afficher la liste des capteurs du site et de la pièce dont on lui passe les noms en argument.

affiche-contenu-site.c:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sqlite3.h>

static int callback_read_int(void * param, int nb_fields, char * value[], char * field[]);
static int callback_capteur(void * unused, int nb_fields, char * value[], char * field[]);

int main(int argc, char *argv[])
{
   sqlite3 * db;
   int ref_site = -1;
   int ref_piece = -1;
   char requete[256];
   if (argc != 4) {
       fprintf (stderr, "usage: %s   \n", argv[0]);
       exit(EXIT_FAILURE);
   }

   if (sqlite3_open_v2(argv[1], & db, SQLITE_OPEN_READONLY, NULL) != 0) {
       fprintf(stderr, "%s: impossible d'ouvrir la base %s\n", argv[0], argv[1]);
       exit(EXIT_FAILURE);
   }

   // Rechercher le numero de reference du site correspondant au nom indique.
   snprintf(requete, 256, "SELECT ref FROM site WHERE nom='%s'", argv[2]);
   if (sqlite3_exec(db, requete, callback_read_int, &ref_site, NULL) != 0) {
       fprintf(stderr, "%s: erreur dans la base %s\n", argv[0], argv[1]);
       sqlite3_close(db);
       exit(EXIT_FAILURE);
   }

   if (ref_site == -1) {
       fprintf(stderr, "%s: Site %s 'non trouve'\n", argv[0], argv[2]);
       sqlite3_close(db);
       exit(EXIT_FAILURE);
   }

   printf("%s\n", argv[2]);
   // Rechercher le numero de reference de la piece correspondant au nom indique.
   snprintf(requete, 256, "SELECT ref FROM piece WHERE ref_site='%d' AND nom='%s'", ref_site, argv[3]);
   if (sqlite3_exec(db, requete, callback_read_int, &ref_piece, NULL) != 0) {
       fprintf(stderr, "%s: erreur dans la base %s\n", argv[0], argv[1]);
       sqlite3_close(db);
       exit(EXIT_FAILURE);
   }

   if (ref_piece == -1) {
       fprintf(stderr, "%s: Piece %s 'non trouvee'\n", argv[0], argv[3]);
       sqlite3_close(db);
       exit(EXIT_FAILURE);
   }

   printf("  %s\n", argv[3]);

   // Parcourir les capteurs de la piece selectionnee.
   snprintf(requete, 256, "SELECT * FROM capteur WHERE ref_piece='%d'", ref_piece);
   if (sqlite3_exec(db, requete, callback_capteur, NULL, NULL) != 0) {
       fprintf(stderr, "%s: Pas de capteur trouve\n", argv[0]);
       sqlite3_close(db);
       exit(EXIT_FAILURE);
   }

   sqlite3_close(db);
   return EXIT_SUCCESS;
}

static int callback_read_int(void * param, int nb_fields, char * value[], char * field[])
{
   int * ival = (int *) param;
   if (sscanf(value[0], "%d", ival) != 1)
       return -1;
   return 0;
}

static int callback_capteur(void * unused, int nb_fields, char * value[], char * field[])
{
   int i;
   printf("    -> ");
   for (i = 0; i < nb_fields; i ++) {
       if (strcmp (field[i], "type") == 0)
           printf(" Type : %s, ", value[i]);
       if (strcmp(field[i], "identifiant") == 0)
           printf(" Identifiant : %s, ", value[i]);
   }
   printf("\n");
   return 0;
}

Il y a plusieurs manières d’utiliser l’API SQLite depuis le langage C ; j’ai choisi d’employer des callbacks qui seront invoquées pour chaque enregistrement trouvé lors d’une requête SELECT.

Il faut compiler notre code avec l’option -lsqlite3 pour le lier avec la bonne bibliothèque. Voyons son utilisation ainsi que les principaux messages d’erreur.

$ cc affiche-contenu-site.c -o affiche-contenu-site -lsqlite3
$ ./affiche-contenu-site
usage: ./affiche-contenu-site   
$ ./affiche-contenu-site domotique.sql "Marco" "Reserve"
./affiche-contenu-site: Site Marco 'non trouve'
$ ./affiche-contenu-site domotique.sql "Restaurant le Marco Polo" "Reserve"
Restaurant le Marco Polo
./affiche-contenu-site: Piece Reserve 'non trouvee'
$ ./affiche-contenu-site domotique.sql "Restaurant le Marco Polo" "Cuisine"
Restaurant le Marco Polo
 Cuisine
    ->  Type : PT100,  Identifiant : 100,
    ->  Type : KZ08,  Identifiant : 101,
$

Bien sûr cet exemple est très simple. Pour une véritable application embarquée, on a plutôt coutume d’écrire un petit module qui va rechercher tous les éléments de configuration dans la base de données et les stocker dans une structure en mémoire afin de ne plus avoir d’accès à réaliser par la suite.

On voit bien l’intérêt de l’emploi d’une base SQLite3 pour des systèmes où une partie de la configuration doit être réalisée par des scripts shell (lors d’une première mise en service ou lors d’une restauration des “factory presets”) ou par une interface HTML depuis le réseau (en invoquant des scripts PHP ou TCL. L’application principale, écrite en C/C++, Java, Python, Ruby, pourra facilement lire et sauvegarder la configuration grâce aux modules écrits pour tous ces langages.

Notons que le fichier représentant la base de données est constitué d’une structure interne binaire pas forcément portable entre systèmes différents. Il est possible de demander à sqlite3 de nous afficher une représentation de la base de données sur sa sortie standard (commande .dump) sous une forme textuelle qui pourra être réinjectée sur une autre machine (commande .restore).

$ sqlite3  domotique.sql  .dump
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE site (
       nom     CHAR(64),
       adresse CHAR(512),
       ref     INTEGER PRIMARY KEY
   );
INSERT INTO "site" VALUES('Restaurant le Marco Polo','4, avenue d''Italie - 75013 Paris',1);
CREATE TABLE piece (
       nom         CHAR(256),
       volume   FLOAT,
       ref_site INTEGER,
       ref      INTEGER PRIMARY KEY,
       FOREIGN KEY (ref_site) REFERENCES site(ref)
   );
INSERT INTO "piece" VALUES('Cuisine',92.4,1,1);
INSERT INTO "piece" VALUES('Salle',148.3,1,2);
CREATE TABLE capteur (
       type        CHAR(32),
       identifiant CHAR(32),
       ref_piece   INTEGER,
       FOREIGN KEY(ref_piece) REFERENCES piece(ref)
       PRIMARY KEY(ref_piece, identifiant)
   );
INSERT INTO "capteur" VALUES('PT100','100',1);
INSERT INTO "capteur" VALUES('KZ08','101',1);
CREATE TABLE actionneur (
       type        CHAR(32),
       identifiant CHAR(32),
       ref_piece   INTEGER,
       FOREIGN KEY(ref_piece) REFERENCES piece(ref)
       PRIMARY KEY(ref_piece, identifiant)
   );
COMMIT;
$

Conclusion

Nous avons parcouru au fil de ces deux articles plusieurs méthodes de chargement et sauvegarde de paramètres depuis une application en privilégiant l’optique “système embarqué”. La plupart de ces éléments peuvent s’appliquer pour des applications fonctionnant dans des environnements plus classiques (postes de travail, serveurs, etc.).

Nos premiers essais avec une sauvegarde binaire “brute” ont rapidement montré leurs limites surtout pour la pérennité et la portabilité des données, la sauvegarde des champs sous forme de textes Ascii (éventuellement avec des sections à la manière des .ini Windows) est plus robuste mais ne permet pas de représenter des informations avec une structure un peu complexe.

L’utilisation d’une bibliothèque comme libconfig permet une représentation arborescente plus poussée, tout comme le format XML qui dispose de nombreux outils d’analyse, examen, vérification de cohérence, etc. Enfin l’emploi d’une base de données offre des possibilités de recherche plus riches encore et une facilité d’intégration dans un environnement de configuration HTML au détriment de la lisibilité directe du fichier par un être humain.

2 Réponses

  1. Seb dit :

    Bonjour,
    Merci pour l’ensemble de vos articles toujours intéressants et pédagogiques.

    La modification de fichier de configuration des systèmes embarqués me ramène à une problématique que j’ai eue dans le passé à savoir comment protégé mon système des arrêts intempestifs lorsqu’il n’est pas en lecture seule.

    A l’époque j’avais utilisé une méthode proche de celle que vous avez décrite dans un article en 2000, aujourd’hui d’autres solutions plus simples existent-t-elles ?
    Bonne continuation

  2. Mathieu dit :

    Il me semble que voila une mission pour la libroxml !
    https://code.google.com/p/libroxml/

    Merci pour l’article

URL de trackback pour cette page