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

#!/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.

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.