But du jeu
La salle serveurs d’Erasme se devait d’être dotée de capteurs pour "monitorer" ce qui se passait à l’intérieur, les serveurs étaient déjà surveillés au niveau logiciel mais pas encore au niveau matériel.
L’importance de cette démarche s’est faite ressentir lors des derniers orages, en effet la climatisation a la fâcheuse habitude de disjoncter et de ne plus redémarrer jusqu’à une intervention humaine. Selon nos mesure la température ambiante de la pièce monte de 2°C par minute à partir du moment où la climatisation s’arrête et cette augmentation est exponentielle...
Afin de surveiller la température de la salle et le bon fonctionnement de la climatisation il a été décidé d’installer une centrale de capteurs reliée à un serveur. Il a ensuite été rajouté un capteur lumière pour savoir si la salle est éclairée ou pas et un capteur pyroélectrique pour détecter les mouvements. Il y a deux capteurs de température : un placé directement à la sortie d’air de la climatisation et un autre au-dessus d’une baie de serveurs pour connaître la température ambiante.
Un relais peut également être piloté depuis le PC et via l’arduino. Il est utilisé pour délencher l’alarme incendie (qui est prévue pour être pilotée ainsi) qui va ensuite se charger d’appeler des personnes lorsque la climatisation s’est arrêtée.
Le fonctionnement se décrit comme tel :
- un arduino est utilisé et programmé pour communiquer au serveur, via le port USB, l’état des différents capteurs
- un démon écrit en C va se charger de demander de temps en temps à l’arduino les valeurs et va éventuellement les modifier pour ensuite les communiquer de différentes manières : en OSC (protocole simple de transfert de valeurs sur UDP), dans un fichier ou sur la sortie standard
- un script bash lancé périodiquement par un cron va consulter le contenu du fichier maintenu par le démon et informer les administrateurs par mail lors d’un évènement critique (mouvements dans la salle, arrêt de la climatisation, lumière restée allumée, température trop élevé), ce script peut aussi avertir via l’alarme en envoyant un signal au démon
Le projet appelé sensorsd se veut d’être une base saine la plus générique possible et peut être réutilisé dans un autre contexte, avec des capteurs différents et dans un but différent.
Résultat (presque) final
Voici quelques photo de l’état actuel du projet. Il va être refait dans un boitier plus grand pour accueillir le relais (qui n’avait pas été prévu initialement).
Archive
- Les sources et le binaire du démon (./daemon)
- La source du programme pour l’arduino (./arduino)
- Le script ("schedulé" avec un cron) utilisé à Erasme (./script)
- L’exemple de configuration utilisé à Erasme (./conf)
A vous d’adapter ce qui doit l’être...
Matériel utilisé
- 1 arduino
- 2 CTN 10 kΩ
- 2 résistances 10 kΩ
- 1 potentiomètre 0 Ω - 50 kΩ
- 3 connecteurs jack femelle et 3 mâle
- 2 LDR/photorésistance
- 1 capteur pyroélectrique
- 1 petite LED
- 1 relais
- 1 boîte de dérivation (minimum 4 raccords)
- 1 PC linux (avec liaison USB)
- fer à souder, étain...
- pistolet à colle (le gaffeur anti court-circuit)
Schémas
Les capteurs de type « résistance variable » (LDR et CTN) sont employés dans un montage de type « diviseur de tension ».
La résistance talon (en série avec le capteur) se calcule approximativement selon la formule
où Rcapteur sont les 2 résistances extrêmes que peut exercer le capteur.
Les capteurs ont été reliés au boîtier électrique à l’aide de fiches Jack 3,5.
- Raccordement des LDR :
La LDR a une plage de valeurs très grande de l’ordre de l’Ω jusqu’au MΩ. (La valeur représentée sur le schéma est donc indicative seulement). Le but est d’obtenir une valeur logique en entrée : 1 si lumière, 0 sinon. Il faut donc pouvoir ajuster le seuil de détection, un potentiomètre est utilisé : il peut monter jusqu’à 10 kΩ dans l’exemple. Une autre LDR a été rajoutée pour savoir si la climatisation est alimentée ou pas (elle a une petite LED rouge allumée quand elle fonctionne, le capteur lumière a été posé directement dessus) et calibrée avec cette fois-ci une résistance fixe de 100 kΩ (la LDR est plus petite et n’a donc pas les mêmes propriétés).
- Raccordement des CTN :
La CTN a une plage de valeurs très grande. Celle utilisée est une 10 kΩ (à environ 25°C) or la température à mesurer se situe aux alentours de 25°C donc nous utilisons une résistance talon de 10 kΩ également pour obtenir une tension en entrée de 2,5 V à 25°C. (Plus de détails sur la relation tension-température dans la suite).
- Raccordement du capteur pyroélectrique :
Il agit comme un capteur logique : 0 = pas de mouvement, 1 = mouvement. On peut donc le modéliser par un bouton poussoir.
- Raccordement de la LED témoin du bon fonctionnement :
Elle est tout simplement (et sans résistance) reliée à une sortie digitale de l’arduino.
- Raccordement du relais
Cf. raccordement de la LED
- Schéma global avec l’arduino au centre (seuls les connecteurs utilisés sont affichés, il manque la LED témoin du bon fonctionnement et le relais) :
Tous les connecteurs d’alimentation et de masse sont reliés ensemble puis reliés à VCC ou à la masse de l’arduino.
Gestion du capteur température
Le capteur température utilisé est une simple CTN : une résistance dont la valeur diminue avec la température.
Elle a été placée dans le pont diviseur de tension comme expliqué auparavant et une liste de valeurs sur l’entrée analogique ont été récupérées expérimentalement avec l’arduino.
1023 = 5V
0 = 0V
On peut en déduire une approximation (max 3°C d’erreur sur la plage 10°C-60°C) linéaire selon une fonction affine :
Valeur = -9,44 * T° + 719,4
~<=> Valeur = -9 * T° + 719 (décimales négligeables...)
<=> T° = (Valeur – 719) / 9
<=> T°= 719 X 2 < - 9 / (expression RPN à utiliser dans la config dans ce cas, « < » correspondant au décalage de bits nécessaire)
Programme arduino
Code : Cf archive
Pour communiquer avec l’arduino un mini-protocole sur un byte est utilisé.
Les capteurs analogiques ont un code attribué de 0 à 5 codé sur 1 byte. Les capteurs logiques sont quant à eux codés de ’a’ à ’n’ pour les 12 pins (le 13ème étant utilisé en sortie pour allumer la LED). Il suffit donc d’envoyer le code correspondant au capteur désiré et l’arduino renverra la valeur correspondante codée sur un byte également.
Les valeurs analogiques s’étendent de 0 à 1023 (de 0V à 5V) et donc de 0 à 10 bits. Or 1 byte = 8 bits. Les valeurs proches de 1023 ne pourront donc pas être codées sur 1 byte. C’est pour cela qu’un décalage de 2 bits vers la droite est effectué pour réduire la « résolution ». La perte maximale est de 11 en binaire (=3 en base 10 "classque") ce qui est négligeable. Il faut par contre penser du côté du PC à faire un décalage de 2 bits sur la gauche.
Les valeurs des capteurs analogiques sont enregistrées en permanence dans un tableau contenant les 10 dernières valeurs de chacun. Cet enregistrement se fait toutes les 200 millisecondes. Quand la valeur d’une entrée analogique est demandée, une moyenne des enregistrements est renvoyée. Le tout afin de lisser les mesures pour éviter les faux-positifs.
Programme en C (linux)
Code : Cf archive
Pour compiler exécuter :
gcc -Wall -lconfuse -llo main.c serial.c rpn.c daemonize.c osc.c -o sensorsd
Pour exécuter le programme il faut avoir la librairie libLo et libConfuse (paquets debian liblo0 et libconfuse0) et pour le compiler il faut rajouter les paquets libl0-dev et libconfuse0-dev (d’où les options -lconfuse et -llo de gcc pour aller chercher /usr/lib/liblo.a et /usr/lib/libconfuse.a)
La librairie libLo permet de gérer la couche de communication OSC et la couche de réseau qui va avec. Elle ne supporte pas malheureusement les envois en broadcast qui sont désactivés par défaut pour les sockets (setsockopt(sock, SOL_SOCKET, SO_BROADCAST, 1, 1) pour y remédier il faudrait inclure cette ligne dans les sources de la librairie).
La libraire libConfuse permet de gérer un fichier de configuration. Les valeurs par défaut (si elles ne sont pas définies dans le fichier) sont configurées dans le code source.
Voici la liste des paramètres disponibles, leur type, leur valeur par défaut et leur description :
- osc_ip_address (string) ["127.0.0.1"] : adresse de destination des paquets OSC
- osc_address (string) ["/sensors/%s"] : adresse OSC de destination des paquets, la chaîne "%s" sera remplacée par le nom du capteur (voir section sensor)
- osc_port (string) ["1234"] : port UDP de destination des paquets OSC (il s’agit bien d’un paramètre de type string, ce n’est pas une erreur !)
- osc_enabled (bool) [FALSE] : si TRUE l’envoi de messages OSC est activé
- arduino_device (string) ["/dev/ttyUSB0"] : adresse du périphérique arduino, si plusieurs arduino ou périphériques de type SerialOverUSB sont connectés il sera peut-être nécessaire de changer ce paramètre. Par contre il se peut que l’arduino se retrouve parfois sur /dev/ttyUSB1, cela arrive parfois si l’USB est débranché et rebranché très vite, pour régler le problème : ne pas modifier la configuration (c’est inutile), débrancher l’arduino, patienter quelques secondes et le rebrancher
- baudrate (int) [9600] : vitesse de la connection série avec l’arduino, doit correspondre à celle choisie dans le programme arduino !
- refresh_time (int) [10] : durée de la pause en secondes entre chaque rafraîchissement des valeurs des capteurs
- daemonize (bool) [TRUE] : le programme se « démonise » si daemonize=TRUE
- status_file (string) ["/var/local/sensorsd/status"] : chemin du fichier mis à jour avec les valeurs des capteurs
- pid_file (string) ["/var/run/sensorsd.pid"] : chemin du fichier contenant le PID du démon et servant à éviter les exécutions de plusieurs démons en parallèle
- user (string) ["sensorsd"] : utilisateur utilisé par sensorsd pour fonctionner, si daemonize est à false le programme tournera avec les droits de l’utilisateur qui l’a lancé et ce paramètre sera ignoré
- group (string) ["dialout"] : même remarque que précédemment, de plus : le paramètre par défaut est correct sur ubuntu mais sur les autres distributions il est vivement recommandé de vérifier qu’il correspond bien au groupe propriétaire de arduino_device
- sensor (section) : sections qui vont paramétrer les différents capteurs
- code (string) : à placer à côté du mot clé « sensor » définissant la section (cf l’exemple)
- name (string) ["Unnamed"] : nom du capteur sans espace (si des espaces sont présents les paquets OSC, et plus particulièrement l’adresse OSC, seront erronés), une valeur par défaut est proposé mais il est très fortement conseillé de la changer (problèmes de capteurs différents apparaîssants comme identiques)
- rpn (string) ["X"] : expression RPN à évaluer, s’il n’y a pas de calcul RPN à effectuer (cas des capteurs logiques par exemple) il suffit de ne pas spécifier ce paramètre
- memorize (bool) [FALSE] : TRUE pour mémoriser la valeur quand elle atteint l’état memorize_state
- memorize_state (bool) [TRUE] : état de l’entrée logique de l’arduino qui fait déclencher l’enregistrement, TRUE=1 et FALSE=0
Les paramètres de type string doivent être entourés de quotes : " "
Une expression RPN peut être appliquée à la valeur des capteurs. 6 opérations sont disponibles : +,-,*,/,<,>. < et > correspondent respectivement au décalage de bits à gauche (<<) et à droite (>>). Il est obligatoire d’utiliser la notation à 1 caractère. La valeur renvoyée par l’arduino remplacera le caractère X dans l’expression. Pour nos capteurs température nous avons utilisé l’expression : "719 X 2 < - 9 /".
Le programme prend en paramètre le chemin du fichier de configuration, si rien n’est spécifié le fichier /etc/sensorsd.conf sera utilisé.
L’ordre des paramètres n’a aucune importance, il faut néanmoins remarquer que les valeurs des capteurs seront affichées et envoyées en OSC dans le même ordre que celui des paramètres.
Le programme en plus d’envoyer un message OSC pour chaque capteur va afficher la valeur de ce capteur (uniquement si daemonize=FALSE) sur la sortie standard (stdout) et l’écrire dans le fichier status_file sous la forme :
sensor_code,sensor_name,valeur
Il s’agit ainsi d’un fichier facilement grep-able pour écrire par dessus un script shell d’alerte etc...
Il est possible de mémoriser un état logique. Par exemple si le capteur de mouvement passe à 1, sa valeur dans le programme (et donc sur stdout, dans status_file et dans les messages OSC) est alors bloquée et restera à 1 même si l’état logique du capteur repasse à 0. En réalité et pour éviter les faux-positifs il faut que la valeur reste à memorize_state pendant 2 cycles, c’est-à-dire qu’une première mesure est faite à T=0 et qu’à la deuxième mesure à T=refresh_time la valeur doit toujours être à memorize_state pour être bloquée. Pour débloquer toutes les valeurs il suffit d’envoyer le signal HUP avec kill -HUP <PID>
.
En remplaçant <PID> par le PID du démon disponible dans pid_file.
Le fonctionnement en mode démon est fortement conseillé. Le programme se détache alors du processus parent. Le but est d’obtenir un serveur autonome. Le but est d’obtenir un serveur autonome. Pour terminer proprement (fermeture des fichiers, libération de la mémoire...) le démon il faut exécuter
kill -INT <PID>
En remplaçant <PID> par le PID du démon disponible dans pid_file.
Quand le programme tourne en mode démon les erreurs et informations ne sont plus affichées, pour identifier un problème il peut être très utile de mettre l’option daemonize à false.
En mode démon, et pour des raisons des sécurité, le programme ne tourne pas en tant que root mais avec l’identité de l’utilisateur user qui est par défaut sensorsd. sensorsd doit exister sur la machine, appartenir au groupe group (pour pouvoir communiquer via le port série) et les fichiers status_file et pid_file doivent lui appartenir.
Le déclenchement du relais s’effectue en envoyant le signal SIGUSR1 au programme.
Le code source est commenté, pour en savoir plus : « may the source be with you ».
Pour la petite histoire : j’ai rencontré quelques problèmes lors du développement au niveau des signaux et je pense qu’il est intéressant d’en parler.
Premier comportement dérangeant : la réception d’un signal termine le sleep() (main.c) en cours, je ne l’avais pas prévu mais c’est bien indiqué dans la page de man... Le principal effet est que la temporisation (entre deux vérifications des valeurs) peut être plus courte que prévue initialement.
Un autre problème plus gênant a été rencontré. Le symptôme : un décalage dans les valeurs entre les différents capteurs après l’utilisation d’un signal. Le problème apparaissait de manière aléatoire. J’ai réussi à le provoquer en envoyant beaucoup de signaux à la fois il est donc paru évident que ce signal devait être reçu à un endroit précis du déroulement du programme pour déclencher le "bug". Après une sorte de mini-profiling du programme (à grands coups de printf()) il s’est avéré que le problème se situait au niveau de l’appel à read() (serial.c, fonction readport()). La réception du signal stoppait le read() bloquant qui renvoyait alors la valeur -1 et ne vidait pas le buffer ce qui avait ensuite pour conséquence le décalage observé... Morale : faire attention aux signal handlers qui peuvent interrompre le programme là où ce n’est pas prévu et toujours vérifier le code retour des appels système (ce que j’avais élégamment esquivé au début).
Programmes utilisés
GNU/Linux
ceux oubliés ou non-cités
A propos de moi
Clément NOTIN, 17 ans, lycéen en section Scientifique option SI au lycée René Cassin.
Mail : clement@notin.org
Remerciements
Je remercie tous ceux qui m’ont accueilli : Michel, Jean-Philippe, Sophie, Daniel, Hélène, Bruno, Patrick, Pierre-Gilles, Yves-Armel Martin.