Club robotique de Sophia-Antipolis

Accueil > POBOTpedia > Ordinateurs embarqués (SBC) > La carte Raspberry PI > Projets complets avec la Raspberry Pi > Bouton d’arrêt sécurisé pour Raspberry Pi

Bouton d’arrêt sécurisé pour Raspberry Pi

mercredi 2 octobre 2013, par Eric P.

Les systèmes embarqués avec un système d’exploitation sur une carte mémoire utilisent des mécanismes d’écriture sur fichiers qui nécessite une procédure d’arrêt spécifique pour ne pas en compromettre le bon fonctionnement, pouvant aller jusqu’à la corruption de la carte SD.

La carte Raspberry Pi utilise un système GNU/Linux qui utilise un certain nombre d’accès aux fichiers de la carte mémoire SD qui stocke tous ses programmes et sa configuration.

Pour éviter de compromettre ce système de fichier et augmenter la durée de vie de votre carte SD, il est nécessaire de procéder à une extinction propre. Ne débranchez jamais l’alimentation de votre carte "à chaud", car vous pourriez perdre toutes les données stockées sur la carte.

La raison en est simple : pour optimiser les performances et moins solliciter le support de stockage, le système n’écrit pas immédiatement les modifications sur le support physique. C’est ce qu’on appelle la mise en cache. Il ne le fait que lorsque le cache est plein, qu’on provoque son écriture en fermant les fichiers en cours d’accès ou bien qu’on fait appel à une fonction système prévue à cet effet. Si on coupe l’alimentation de la Raspberry alors que les données en attente n’ont pas été écrites sur le support mémoire, les fichiers concernés ont de fortes chances d’être laissés dans un état incohérent, se traduisant dans le meilleur des cas par la perte d’une partie de leur contenu.

Mais il y a pire, car les structures de données utilisées par le système pour gérer le contenu des répertoires sont traitées de la même manière. Les conduire à un état incohérent peut se solder par l’impossibilité d’accéder à tout ou partie du contenu du support. A ne pas prendre à la légère par conséquent.

Une manière d’éviter cela lorsqu’on n’a pas besoin de sauver des données sur la carte est de "monter" le système de fichiers en lecture seule. Plus aucun risque de le corrompre alors. Mais cette stratégie n’est pas toujours applicable.

Procéder à un arrêt du système dans les règles est très facile quand on se connecte à distance ou bien que la carte est reliée à un clavier et un écran, car il suffit d’utiliser la commande poweroff ou halt tout simplement. Mais ce n’est plus possible si la carte est embarquée et sans interface utilisateur, comme c’est le cas de la plupart de nos robots mobiles.

Il faut donc une solution à la fois matérielle et logicielle : un bouton d’arrêt (contact) sur une broche d’entrée du connecteur GPIO et un code "daemon" qui observe ce qui se passe sur cette patte pour déclencher l’arrêt logiciel automatiquement.

Une bonne mise en pratique de nos ateliers sur le langage Python, les entrées/sorties de la Raspberry et les mécanismes de GNU/Linux.

Le code du daemon

  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3.  
  4. import os, sys
  5. if not os.getuid() == 0:
  6.     sys.exit('Needs to be root for running this script.')
  7.  
  8. import RPi.GPIO as GPIO
  9. import time
  10. import subprocess
  11.  
  12. # the button is connected on GPIO4 (pin 7 of header)
  13. BTN_IO = 4
  14. # we use the Broadcom numbering of the I/O
  15. # (instead of the RasPi header pin numbering)
  16. GPIO.setmode(GPIO.BCM)
  17. # the I/O is configured as input with pullup enabled
  18. GPIO.setup(BTN_IO, GPIO.IN, GPIO.PUD_UP)
  19.  
  20. print('monitoring started')
  21. while True:
  22.     pressed = (GPIO.input(BTN_IO) == 0)
  23.     if pressed:
  24.         time.sleep(4)
  25.         pressed = (GPIO.input(BTN_IO) == 0)
  26.         if pressed:
  27.             break
  28.     else:
  29.         time.sleep(0.1)
  30.  
  31.  
  32. print('Shutdown button pressed. System is going to halt now')
  33. subprocess.call('halt')

Télécharger

Note : oui, je sais, les commentaires sont en Anglais. Désolé, déformation professionnelle, mais aussi plus grande compacité des textes. Difficile de changer d’habitudes à mon âge ;)

Ce code très simple effectue les tâches suivantes :

  • vérification qu’il s’exécute en tant que root (car la commande halt utilisée pour déclencher l’arrêt du système nécessite ce privilège)
  • configuration de l’I/O utilisée (ici la GPIO4, correspondant à la pin 7 du connecteur de la Raspberry) en mode input, avec la résistance de pull-up interne activée, ce qui permet de se contenter de connecter la GPIO à la masse via le bouton poussoir sans nécessiter d’autre composant
  • entrer dans une boucle infinie, qui teste toutes les 100ms si le bouton a été appuyé
  • dans ce cas, passer dans la branche de confirmation qui vérifie s’il l’est toujours 4 secondes plus tard
  • si c’est le cas, quitter la boucle et invoquer la commande système halt

Le résultat est que le shutdown sera effectué si le bouton est maintenu appuyé au moins 4 secondes (en fait, si on le relâche entre temps, ça marche aussi, mais cette manoeuvre pouvant difficilement se faire par mégarde, cela apporte un niveau de sécurité équivalent).

Sauvegardez le script sous /usr/local/bin/monitor-halt-btn.py par exemple (si vous choisissez autre chose, modifier en conséquence le script init ci-après), et ne pas oublier de le rendre exécutable (chmod +x ...) afin de simplifier la ligne d’invocation dans ce qui suit (sinon, il suffit de la préfixer par un appel explicite à l’interpréteur Python).

Le lancement automatique

Avoir un daemon c’est bien, mais encore faut-il qu’il soit lancé automatiquement au démarrage du système. C’est le rôle du script init halt-button ci-après.

  1. #! /bin/sh
  2. ### BEGIN INIT INFO
  3. # Provides:          halt-button
  4. # Required-Start:    $remote_fs $syslog
  5. # Required-Stop:     $remote_fs $syslog
  6. # Default-Start:     2 3 4 5
  7. # Default-Stop:      0 1 6
  8. # Short-Description: Halt button monitoring service
  9. # Description:       Manages the deamon which monitors a push button
  10. #                    grounding GPIO4 to initiate the system halt sequence
  11. ### END INIT INFO
  12.  
  13. # Author: Eric Pascual <eric@pobot.org>
  14.  
  15. # Do NOT "set -e"
  16.  
  17. # PATH should only include /usr/* if it runs after the mountnfs.sh script
  18. PATH=/sbin:/usr/sbin:/bin:/usr/bin
  19. DESC="Halt button monitoring service"
  20. NAME=halt-button
  21. DAEMON=/usr/local/bin/monitor-halt-btn.py
  22. DAEMON_ARGS=""
  23. PIDFILE=/var/run/$NAME.pid
  24. SCRIPTNAME=/etc/init.d/$NAME
  25.  
  26. # Exit if the package is not installed
  27. [ -x "$DAEMON" ] || exit 0
  28.  
  29. # Read configuration variable file if it is present
  30. [ -r /etc/default/$NAME ] && . /etc/default/$NAME
  31.  
  32. # Load the VERBOSE setting and other rcS variables
  33. . /lib/init/vars.sh
  34. # force VERBOSE
  35. VERBOSE=yes
  36.  
  37. # Define LSB log_* functions.
  38. # Depend on lsb-base (>= 3.2-14) to ensure that this file is present
  39. # and status_of_proc is working.
  40. . /lib/lsb/init-functions
  41.  
  42. #
  43. # Function that starts the daemon/service
  44. #
  45. do_start()
  46. {
  47.         # Return
  48.         #   0 if daemon has been started
  49.         #   1 if daemon was already running
  50.         #   2 if daemon could not be started
  51.  
  52.         [ -f "$PIDFILE" ] && return 1
  53.         $DAEMON $DAEMON_ARGS > /dev/null &
  54.         echo "PID=$!" > $PIDFILE
  55. }
  56.  
  57. #
  58. # Function that stops the daemon/service
  59. #
  60. do_stop()
  61. {
  62.         # Return
  63.         #   0 if daemon has been stopped
  64.         #   1 if daemon was already stopped
  65.         #   2 if daemon could not be stopped
  66.         #   other if a failure occurred
  67.  
  68.         [ -r "$PIDFILE" ] || return 1
  69.         . $PIDFILE
  70.         kill $PID
  71.         rm -f $PIDFILE
  72.         return 0
  73. }
  74.  
  75. case "$1" in
  76.   start)
  77.         [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
  78.         do_start
  79.         case "$?" in
  80.                 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
  81.                 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
  82.         esac
  83.         ;;
  84.   stop)
  85.         [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
  86.         do_stop
  87.         case "$?" in
  88.                 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
  89.                 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
  90.         esac
  91.         ;;
  92.   status)
  93.         [ "$VERBOSE" != no ] && log_daemon_msg "$DESC is"
  94.         pgrep -f $DAEMON > /dev/null
  95.         if [ $? -eq 0 ] ; then
  96.                 [ "$VERBOSE" != no ] && log_progress_msg "running" ; log_success_msg
  97.                 exit 0
  98.         else
  99.                 [ "$VERBOSE" != no ] && log_progress_msg "not running" ; log_failure_msg
  100.                 exit 1
  101.         fi
  102.         ;;
  103.   restart|force-reload)
  104.         #
  105.         # If the "reload" option is implemented then remove the
  106.         # 'force-reload' alias
  107.         #
  108.         log_daemon_msg "Restarting $DESC" "$NAME"
  109.         do_stop
  110.         case "$?" in
  111.           0|1)
  112.                 do_start
  113.                 case "$?" in
  114.                         0) log_end_msg 0 ;;
  115.                         1) log_end_msg 1 ;; # Old process is still running
  116.                         *) log_end_msg 1 ;; # Failed to start
  117.                 esac
  118.                 ;;
  119.           *)
  120.                 # Failed to stop
  121.                 log_end_msg 1
  122.                 ;;
  123.         esac
  124.         ;;
  125.   *)
  126.         echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
  127.         exit 3
  128.         ;;
  129. esac

Télécharger

Ce script utilise le format classique des init Debian et équivalents. Je vous engage à vous reporter à la documentation officielle sur le sujet pour les détails.

Quelques remarques :

  • si vous enregistrez le script Python sous un autre nom et/ou emplacement que ceux illustrés précédemment, modifiez la définition de la variable DAEMON (ligne 21) en conséquence
  • plutôt que de faire appel aux fonctions start_stop_daemon et ses copines définies dans /lib/lsb/init-functions (d’où le fait que ce fichier soit sourcé en ligne 40), nous passons les commandes de lancement du script et nous gérons la mémorisation de son PID directement (fonction do_start)
  • idem pour do_stop
  • à part cela rien de spécial

Mise en place

Le script Python est à placer dans /usr/local/bin si on veut respecter les conventions d’usage, mais comme dit plus haut ce n’est pas une obligation du moment qu’on modifie le script init en conséquence.

Le script init est à rendre exécutable et à placer dans /etc/init.d. A noter que dans les deux cas de figure, on peut utiliser un lien symbolique vers l’emplacement réel des fichiers, par exemple si on souhaite les laisser groupés dans un répertoire sous le home dir de votre utilisateur de travail (cela évite de devoir passer root si on veut les éditer)

Pour activer le script lors de l’init, il faut utiliser la commande :

# update-rc.d halt-button defaults 99

(99 permet de le lancer dans les tous derniers scripts)

Et c’est tout. Ca marche comme ça.

La vignette de cet article est d’ailleurs la photo du bouton d’arrêt de la RasPi qui pilote un démonstrateur pédagogique de l’utilisation de la trigonométrie, via son application à un système simple de balises goniométriques.

JPEG - 398.6 ko
Le bouton d’arrêt du démonstrateur de balises gonio

Mode d’emploi

Il suffit de tenir appuyé le bouton pendant au moins 4 secondes comme déjà indiqué, et d’attendre ensuite que les divers voyants d’activité et autre cessent de clignoter (seule la LED power doit rester allumée). Vous pouvez alors couper l’alimentation de la RasPi en toute sécurité.

Vos commentaires

  • Le 27 mars 2018 à 14:01, par Papylinux En réponse à : Bouton d’arrêt sécurisé pour Raspberry Pi

    Bonjour,
    J’ai installé avec succès ce bouton d’arrêt du raspi il y a quelques années.
    Aujourd’hui, pour l’installer, j’aurais besoin de prendre le +5V à partir de l’alimentation,
    au lieu du +3.3V pris sur le GPIO.
    Est-ce possible ?? Si oui, quelles modifications doit-on apporter pour ne pas "cramer" le GPIO.
    Merci pour votre aide.
    Cordialement.

    • Le 27 mars 2018 à 14:06, par Eric P. En réponse à : Bouton d’arrêt sécurisé pour Raspberry Pi

      Bonjour,
      Je ne sais pas trop ce que vous voulez dire par "prendre le +5V". Le mieux serait de fournir un schéma électronique du montage envisagé, et de nous l’envoyer à notre adresse mail de contact.
      Dans tous les cas les GPIO de la RPi ne sont pas "5V tolerent" du tout, ce qui veut dire que le signal doit rester sous les 3.3V. Plusieurs solutions sont possibles, dont les level shifters, mais la plus simple et la plus économique rest un simple pont diviseur résistif.
      Cordialement

    Répondre à ce message

  • Le 9 octobre 2014 à 10:37, par Testa En réponse à : Bouton d’arrêt sécurisé pour Raspberry Pi

    # the button is connected on GPIO4 (pin 7 of header)
    BTN_IO = 4
    a mon humble avis, il ya une chtite erreur ...

    • Le 9 octobre 2014 à 15:00, par Eric P. En réponse à : Bouton d’arrêt sécurisé pour Raspberry Pi

      Si vous faites référence au fait qu’on écrit "GPIO4" et "pin 7" dans la même phrase, je ne pense pas qu’il y ait une ch’tite erreur, mais je pense plutôt que vous confondez les deux modes d’identification des GPIO de la Raspberry : le mode carte (aka board) qui utilise la numérotation des pins du header et le mode processeur (aka Broadcom) qui utilise la numérotation des pins du chip.
      Comme l’indiquent les 3 lignes de source (et c’est explicite dans le commentaire) après celle qui est censée contenir une erreur, mon script utilise le référencement processeur (BCM) et l’I/O qui est manipulée est bien l’I/O 4 du MCU, qui est accessible à la pin 7 du header.
      De toute manière, ce script fonctionne déjà tel quel sur un certain nombre de nos RasPi. Alors s’il y avait eu une ch’tite erreur, je pense que nous nous en serions rendu compte 😉
      Il se peut que je n’aie pas compris à quelle erreur vous fassiez référence, et que ce qui précède ne soit donc pas la bonne réponse. Merci de préciser votre message initial dans ce cas, car s’il y a vraiment une erreur, je suis très intéressé à savoir laquelle afin de la corriger au plus vite.
      Cordialement

    Répondre à ce message

  • Le 10 juin 2014 à 01:14, par lepile En réponse à : Bouton d’arrêt sécurisé pour Raspberry Pi

    Merci !
    🙂

    Répondre à ce message

Un message, un commentaire ?

modération a priori

Attention, votre message n’apparaîtra qu’après avoir été relu et approuvé.

Qui êtes-vous ?

Pour afficher votre trombine avec votre message, enregistrez-la d’abord sur gravatar.com (gratuit et indolore) et n’oubliez pas d’indiquer votre adresse e-mail ici.

Ajoutez votre commentaire ici

Ce champ accepte les raccourcis SPIP {{gras}} {italique} -*liste [texte->url] <quote> <code> et le code HTML <q> <del> <ins>. Pour créer des paragraphes, laissez simplement des lignes vides.