Club robotique de Sophia-Antipolis

Accueil > Robopedia > Constituants > Les ordinateurs embarqués > 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 compromettre le bon fonctionnement, voire éviter un crash complet irrémédiable.

La carte Raspberry Pi utilise un système GNU/Linux qui ouvre 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 le système et augmenter la durée de vie de votre RPi, il est nécessaire de prévoir une extinction propre. Ne débranchez jamais l’alimentation de votre carte "à chaud", c’est très mauvais et vous pourriez perdre toutes vos données et vos codes sources, sans compter le temps nécessaire pour remettre le système en état opérationnel.

C’est très simple quand on se connecte à distance ou bien que la carte est connectée à un clavier et un écran, mais c’est quasiment impossible la carte est embarquée et autonome comme c’est le cas 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

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os, sys
if not os.getuid() == 0:
    sys.exit('Needs to be root for running this script.')
import RPi.GPIO as GPIO
import time
import subprocess
# the button is connected on GPIO4 (pin 7 of header)
BTN_IO = 4
# we use the Broadcom numbering of the I/O
# (instead of the RasPi header pin numbering)
GPIO.setmode(GPIO.BCM)
# the I/O is configured as input with pullup enabled
GPIO.setup(BTN_IO, GPIO.IN, GPIO.PUD_UP)
print('monitoring started')
while True:
    pressed = (GPIO.input(BTN_IO) == 0)
    if pressed:
        time.sleep(4)
        pressed = (GPIO.input(BTN_IO) == 0)
        if pressed:
            break
    else:
        time.sleep(0.1)
print('Shutdown button pressed. System is going to halt now')
subprocess.call('halt')

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.

#! /bin/sh
### BEGIN INIT INFO
# Provides:          halt-button
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Halt button monitoring service
# Description:       Manages the deamon which monitors a push button
#                    grounding GPIO4 to initiate the system halt sequence
### END INIT INFO
# Author: Eric Pascual <eric@pobot.org>
# Do NOT "set -e"
# PATH should only include /usr/* if it runs after the mountnfs.sh script
PATH=/sbin:/usr/sbin:/bin:/usr/bin
DESC="Halt button monitoring service"
NAME=halt-button
DAEMON=/usr/local/bin/monitor-halt-btn.py
DAEMON_ARGS=""
PIDFILE=/var/run/$NAME.pid
SCRIPTNAME=/etc/init.d/$NAME
# Exit if the package is not installed
[ -x "$DAEMON" ] || exit 0
# Read configuration variable file if it is present
[ -r /etc/default/$NAME ] && . /etc/default/$NAME
# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh
# force VERBOSE
VERBOSE=yes
# Define LSB log_* functions.
# Depend on lsb-base (>= 3.2-14) to ensure that this file is present
# and status_of_proc is working.
. /lib/lsb/init-functions
#
# Function that starts the daemon/service
#
do_start()
{
        # Return
        #   0 if daemon has been started
        #   1 if daemon was already running
        #   2 if daemon could not be started
        [ -f "$PIDFILE" ] && return 1
        $DAEMON $DAEMON_ARGS > /dev/null &
        echo "PID=$!" > $PIDFILE
}
#
# Function that stops the daemon/service
#
do_stop()
{
        # Return
        #   0 if daemon has been stopped
        #   1 if daemon was already stopped
        #   2 if daemon could not be stopped
        #   other if a failure occurred
        [ -r "$PIDFILE" ] || return 1
        . $PIDFILE
        kill $PID
        rm -f $PIDFILE
        return 0
}
case "$1" in
  start)
        [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
        do_start
        case "$?" in
                0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
                2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
        esac
        ;;
  stop)
        [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
        do_stop
        case "$?" in
                0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
                2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
        esac
        ;;
  status)
        [ "$VERBOSE" != no ] && log_daemon_msg "$DESC is"
        pgrep -f $DAEMON > /dev/null
        if [ $? -eq 0 ] ; then
                [ "$VERBOSE" != no ] && log_progress_msg "running" ; log_success_msg
                exit 0
        else
                [ "$VERBOSE" != no ] && log_progress_msg "not running" ; log_failure_msg
                exit 1
        fi
        ;;
  restart|force-reload)
        #
        # If the "reload" option is implemented then remove the
        # 'force-reload' alias
        #
        log_daemon_msg "Restarting $DESC" "$NAME"
        do_stop
        case "$?" in
          0|1)
                do_start
                case "$?" in
                        0) log_end_msg 0 ;;
                        1) log_end_msg 1 ;; # Old process is still running
                        *) log_end_msg 1 ;; # Failed to start
                esac
                ;;
          *)
                # Failed to stop
                log_end_msg 1
                ;;
        esac
        ;;
  *)
        echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
        exit 3
        ;;
esac

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é.

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 formulaire accepte les raccourcis SPIP [->url] {{gras}} {italique} <quote> <code> et le code HTML <q> <del> <ins>. Pour créer des paragraphes, laissez simplement des lignes vides.

Ajouter un document