Программирование, радиоэлектроника,
саморазвитие и частичка из моей жизни здесь...

Безопасное завершение работы Raspbian кнопкой, подключенной к Raspberry Pi

Расскажу о создании несложного скрипта и службы для systemd, которые будут отслеживать нажатие подключенной к GPIO кнопки и запускать процесс завершения работы операционной системы Raspbian, что потом позволит безопасно отключить питание Raspberry Pi.

Программа будет написана на языке Python3, также будет создан небольшой файл-юнит (задание) для системного менеджера GNU/Linux Raspbian - Systemd. Опишу реализацию простого примера с одной кнопкой, а также более сложного - с выводом сообщения на OLED-дисплей SD1306.

Содержание:

Для чего это нужно

На основе мини-компьютера Raspberry Pi можно строить самые различные по возможностям и сложности проекты: системы автоматизации для умного дома, сетевой маршрутизатор, мини сервер, систему сбора телеметрии, домашний робот, станок с ЧПУ, устройства IOT (Internet Of Things) и многое другое.

При проектировании подобных устройств очень часто возникает необходимость выключать компьютер Raspberry Pi в автоматическом или ручном режиме, например:

  • при разряде питающих батарей;
  • при завершении сбора каких-то данных в указанное время;
  • просто для экономии электричества;
  • и т.п.

Как и для любого другого компьютера, работу и взаимодействие с аппаратным обеспечением малинки выполняет операционная система (ОС), в данном случае это - Raspbian GNU/Linux.

ОС отвечает за обмен данными между процессором, памятью, накопителями данных и различными периферийными интерфейсами. В качестве накопителя данных в малинке по умолчанию используется Micro-SD карта, которая изготовлена по флешь-технологии.

Перед выключением питания модуля Raspberry Pi необходимо выполнить корректное завершение работы ОС. Непредвиденное или случайное отключение питания платформы может создать не мало проблем:

  • повредить структуру файловых систем на накопителях;
  • привести к потере важных данных;
  • сделать загрузку ОС невозможной;
  • повредить карту Micro-SD;
  • вывести из строя какое-то периферийное устройство.

Для ручного завершения работы ОС на компьютере можно использовать опции графической оболочки, в Raspbian также есть такая возможность: это можно сделать из графической оболочки или же через консоль командой "poweroff".

На корпусах стационарных компьютеров и ноутбуков, как правило, присутствует кнопка управления питанием, которая позволяет включать и выключать компьютер используя для этого интерфейс ACPI (Advanced Configuration and Power Interface).

В случае с малинкой, такая кнопка пока что заранее не предвидена производителем. Завершение работы ОС можно удобно выполнять из графического менеджера или же из консоли, подключившись к малинке локально или удаленно по SSH (Secure SHell).

В случае когда малинка работает без графической оболочки и подключенного дисплея (как сервер, роутер и т.п.) не всегда удобно подключаться к ней каждый раз с другого компьютера и завершать работу ОС консольной командой "poweroff". Здесь возможно что очень не хватает той самой аппаратной кнопки завершения работы, которая присутствует на большинстве компьютеров.

Подключением и настройкой работы такой кнопки к в Raspberry Pi мы и займемся в этой статье.

Как это работает

Суть идеи очень проста: отслеживать событие по нажатию на кнопку и запускать процесс корректного завершения ОС, что в последствии позволит безопасно выключить питание платформы. Для пояснения принципов реализации данного метода я нарисовал небольшую блок-схему:

Завершение работы Raspbian и выключение Raspberry Pi используя кнопку, GPIO, Systemd и Python3

Рис. 1. Завершение работы Raspbian и выключение Raspberry Pi используя кнопку, GPIO, Systemd и Python3.

При запуске операционной системы (ОС) Raspbian обрабатывается задание (юнит, unit) для Systemd, которое в свою очередь запускает программу-скрипт на языке Python3.

Этот скрипт после своего запуска остается работать в фоновом режиме как служба (демон), с реализацией этой возможности нам поможет модуль для python под названием "daemonize".

В программе скрипта выполняется отслеживание состояния одного из каналов GPIO c использованием прерываний, это возможно благодаря модулю "RPi.GPIO".

К отслеживаемому каналу подвязан физический пин на гребенке контактов GPIO в Raspberry Pi, к этому пину и подключена кнопка. После нажатия на кнопку сработает функция прерывания, которая запустит команду завершения работы "poweroff" с системными привилегиями. 

Таким образом, ОС сможет корректно завершить все задачи, закрыть сетевые подключения и открытые файловые дескрипторы, отмонтировать файловые системы и подать сигнал аппаратного выключения платформе Raspberry Pi.

Давайте попробуем реализовать данный функционал в минимальной конфигурации, но прежде нужно провести некоторые подготовительные работы.

Подготовка

Как уже было сказано выше, для работы нам понадобится Python3 и два модуля - "daemonize" и "RPi.GPIO". Первый модуль отсутствует в установке Raspbian по умолчанию, а второй есть в наличии, но его также желательно обновить.

Выполним эти задачи следующими командами в консоли Raspbian:

sudo apt-get update
sudo apt-get install python3 python3-pip python3-rpi.gpio
pip3 install daemonize

Проверим доступность установленных компонентов, для этого запустим интерпретатор Python 3:

python3

Должна быть выведена версия python (3.x) и отображена строка для запросов ">>>". Теперь выполним импорт библиотек и получим некоторую полезную информацию.

Команды, которые нужно вводить, начинаются в каждой строке после символов ">>>", остальные же данные из примера ниже - это выводимая этими командами информация:

>>> import RPi.GPIO as GPIO
>>> GPIO.VERSION
'0.6.3'
>>> GPIO.RPI_INFO
{'REVISION': 'a02082', 'RAM': '1024M', 'TYPE': 'Pi 3 Model B', 'MANUFACTURER': 'Sony', 'PROCESSOR': 'BCM2837', 'P1_REVISION': 3}
>>> from daemonize import Daemonize
>>>

Если все запустилось успешно и не было никаких сообщений об ошибках, то можно двигаться дальше.

О том, для чего нужен модуль "RPi.GPIO" и как с ним работать, я уже подробно рассказывал в предыдущей статье, а вот модуль "daemonize" - это что-то новое.

Библиотека "daemonize" позволяет превратить программу, написанную на языке Python, в процесс, который может работать на Unix-подобных операционных системах.

С ее помощью мы сможем "деймонизировать" написанную программу, которая после своего запуска продолжит работать в фоне и выполнять нужные нам полезные задачи. Она будет работать как процесс - со своими свойствами и PID (Process ID).

Подключение кнопки

Правила и особенности подключении кнопки к GPIO в Raspberri Pi я уже описывал в прошлых статьях - статья 1, статья 2. Здесь мы также будем подключать кнопку используя два резистора, как в примере из "статьи 2".

Схема безопасного подключения кнопки к GPIO в Raspberry Pi

Рис. 2. Схема безопасного подключения кнопки к GPIO в Raspberry Pi.

Здесь будет использован канал IO12, вы же можете использовать любой другой доступный канал, изменив его номер в коде программы.

Исходный код программы

Первым делом нужно определиться где будет храниться файл с программой (скрипт), написанной на языке программирования Python 3. В операционной системе GNU/Linux для этой цели неплохо подойдут директории "/usr/local/bin/" и "/usr/local/sbin/".

В последней из них я уже предлагал выполнять хранение скрипта с правилами для брандмауэра IPTables. Давайте здесь также используем эту же директорию.

Откроем на редактирование новый файл, который потом будет сохранен в директории "/usr/local/sbin/", и назовем его "gpio-poweroff-daemon.py":

sudo nano /usr/local/sbin/gpio-poweroff-daemon.py

Код программы достаточно простой, он выглядит следующим образом:

#!/usr/bin/env python3 
# -*- coding: utf-8 -*-
#
# Raspberry Pi Power-OFF daemon for switch on GPIO
# https://ph0en1x.net

import RPi.GPIO as GPIO

from daemonize import Daemonize
from subprocess import call
from time import sleep

def poweroff_callback(channel):   
    """ GPIO theraded callback function. """
    call('poweroff', shell=True)

def main():
    """ Main program function. """
    print('Power-Switch watchdog started.')
    GPIO.setmode(GPIO.BCM) 
    GPIO.setup(12, GPIO.IN)
    GPIO.add_event_detect( 
        12, 
        GPIO.FALLING, 
        callback=poweroff_callback, 
        bouncetime=300)
    # Main program cycle.
    while True:
        sleep(5)

daemon = Daemonize(
    app="power-switch", 
    pid='/tmp/gpio-poweroff-daemon.pid', 
    action=main)
daemon.start()

Для выхода из редактора "nano" нажимаем сочетание клавиш CTRL+X, потом подтверждаем сохранение содержимого файла клавишей X.

Чтобы скрипт можно было запускать на исполнение, назначим ему соответствующие права (x = eXecute):

sudo chmod +x /usr/local/sbin/gpio-poweroff-daemon.py

Запускаем скрипт и проверить его работоспособность:

sudo /usr/local/sbin/gpio-poweroff-daemon.py

Итак, скрипт запущен и работает как фоновый процесс. Узнать ID этого процесса (PID, число) можно одной из следующих команд:

ps ax | grep [g]pio-poweroff-daemon | awk '{print $1}'
cat /tmp/gpio-poweroff-daemon.pid

Зная ID процесса его можно принудительно завершить:

sudo kill -9 `cat /tmp/gpio-poweroff-daemon.pid`

В момент когда скрипт работает, нажав на подключенную к каналу 12 кнопку, операционная система Raspbian запустит команду "poweroff" и тем самым начнет процесс завершения работы и подготовки к выключению малинки.

Чтобы повторно загрузить ОС, нужно отключить малинку от питания +5В и потом снова включить.

Для автоматизации запуска скрипта при загрузке ОС перейдем к следующему пункту.

Скрипт-задание для systemd

Для автоматического запуска скрипта, написанного на Python, при загрузке ОС мы создадим новый юнит (задание) для системного менеджера GNU/Linux - systemd. Данный файл будет размещен в директории "/etc/systemd/system/", где systemd сможет успешно обнаружить и обработать наш файл с заданием.

Откроем новый файл для редактирования:

sudo nano /etc/systemd/system/gpio-poweoff.service

Поместим в него следующее содержимое:

[Unit]
Description=GPIO Power-OFF Daemon
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/gpio-poweroff-daemon.py
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

Разрешим менеджеру systemd обработку этого файла с заданием:

sudo systemctl enable gpio-poweoff.service

Запустим задание на выполнение:

sudo systemctl start gpio-poweoff.service

Теперь мы можем посмотреть статус задания:

systemctl status gpio-poweoff.service

Будут отображены пути к файлу со скриптом и к файлу с юнитом, текущее состояние процесса и его PID, а также логи выполнения задания.

Перезагружаем Raspbian и дополнительно проверяем статус задания "gpio-poweroff.service", а также работу кнопки, которая должна отреагировать на свое нажатие завершением работы системы.

Пример с применением экрана SSD1306

В этом примере мы будем использовать OLED-дисплей SSD1306 для вывода сообщения с обратным отсчетом до начала запуска процедуры завершения работы системы.

Алгоритм работы: при нажатии на кнопку выводим на экран сообщение о завершении работы, а также числа от 4 до 1 с задержкой 1 секунда (отсчет), потом запускаем команду для завершения работы - poweroff.

Схема подключения кнопки к малинке остается той же самой что и раньше (смотрим рисунок 2). Останется лишь дополнительно подключить OLED-дисплей. О том, как работать с дисплеем SSD1306, подключенным по шине I2C, я уже подробно рассказывал в одной из статей. Там же показана схема подключения дисплея к Raspberry Pi 2, для 3-й версии - все аналогично.

Включим шину I2C:

sudo raspi-config

Идем по пунктам меню: “Advanced Options” - “I2C” - "Yes" - жмем "Finish".  Перезагружаем ОС командой "reboot".

Установим необходимые пакеты для работы с шиной I2C:

sudo apt-get update
sudo apt-get install -y python-smbus i2c-tools

Проверяем определился ли подключенный к шине I2C дисплей SSD1306:

sudo i2cdetect -y 1

Пример вывода команды:

     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- 3c -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: 50 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- 68 -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --

В моем случае, адрес дисплея SSD1306 - "3c" (0x3c).

Больше информации по шине I2C и работе с ней вы можете узнать из статьи про I2C в Raspberry Pi и PCF8591.

Для работы с дисплеем будем использовать старую, но проверенную версию "0.2.0" библиотеки от Ричарда Хала. Создадим для ее загрузки и распаковки временную директорию:

cd /tmp && mkdir ssd1306 && cd ssd1306

Выполним загрузку архива с библиотекой и распакуем его:

wget -c "https://ph0en1x.net/uploads/File/ssd1306-0.2.0-no-doc.tar.gz"
tar -xf ssd1306-0.2.0-no-doc.tar.gz

В папку "/usr/local/sbin/", где находится скрипт "gpio-poweroff-daemon.py", необходимо скопировать из этой библиотеки два файла - "device.py" и "render.py". Выполним копирование файлов, добавив к их именам префикс "oled_":

sudo cp oled/device.py /usr/local/sbin/oled_device.py
sudo cp oled/render.py /usr/local/sbin/oled_render.py

Также нам понадобится библиотека "Pillow", установим ее:

sudo pip3 install Pillow

Для новой программы будем использовать тот же скрипт, что и в предыдущем примере:

sudo nano /usr/local/sbin/gpio-poweroff-daemon.py

Нужно заменить все содержимое файла (для быстрого удаления строк используйте комбинацию клавиш CTRL+K) следующим кодом:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Raspberry Pi Power-OFF daemon for switch on GPIO (v2)
# https://ph0en1x.net

import RPi.GPIO as GPIO

from daemonize import Daemonize
from oled_device import ssd1306
from oled_render import canvas
from PIL import ImageFont
from subprocess import call
from time import sleep

poweroff_is_enabled = False

def poweroff_callback(channel):
    """ GPIO theraded callback function. """
    global poweroff_is_enabled
    poweroff_is_enabled = True

def message():
    """ Show countdown message on display. """
    device = ssd1306(port=1, address=0x3C)
    font_path = '/usr/share/fonts/truetype/noto/NotoMono-Regular.ttf'
    font = ImageFont.truetype(font_path, 20)
    for i in reversed(range(0,5)):
        with canvas(device) as draw:
            draw.text((2, 0), "Shutdown...", font=font, fill=255)
            count_str = str(i) if i else 'Bye!'
            draw.text((50, 30),  count_str, font=font, fill=255)
        sleep(1)

def main():
    """ Main program function. """
    print('Power-Switch watchdog started.')
    global poweroff_is_enabled
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(12, GPIO.IN)
    GPIO.add_event_detect(
        12,
        GPIO.FALLING,
        callback=poweroff_callback,
        bouncetime=300)
    # Main program cycle.
    while True:
        if poweroff_is_enabled:
            message()
            sleep(1)
            call('poweroff', shell=True)
        sleep(1)
 
daemon = Daemonize(
    app="power-switch",
    pid='/tmp/gpio-poweroff-daemon.pid',
    action=main)
daemon.start()

Для запуска скрипта будет использоваться уже созданный в предыдущем примере юнит "gpio-poweoff.service". Запуск, остановку и просмотр статуса программы в ручном режиме можно выполнить следующими командами:

sudo systemctl start gpio-poweoff.service
sudo systemctl status gpio-poweoff.service
sudo systemctl stop gpio-poweoff.service

В этой программе была добавлена глобальная переменная "poweroff_is_enabled", которая при вызове прерыванием функции обратной связи"poweroff_callback" изменит свое значение на "True".

Состояние глобальной переменной "poweroff_is_enabled" отслеживается в основном цикле программы, если оно изменится на "True" - будет запущена функция "message", которая выведет на экран сообщение с отсчетом. После завершения отсчета, система запустит команду "poweroff", которая начнет процедуру завершения работы ОС.

В функции для вывода сообщений на экран SSD1306 использован внешний файл с TTF-шрифтом, который расположен по пути: '/usr/share/fonts/truetype/noto/NotoMono-Regular.ttf'.

Использование TTF-шрифта позволило задать размер для символов и сделать их более удобными к прочтению. В системной папке '/usr/share/fonts/truetype/' есть и другие шрифты, которые вы можете использовать:

ls /usr/share/fonts/truetype/

Список папок с TTF-шрифтами, доступными на моей Raspbian:

dejavu  droid  freefont  lato  liberation2  noto  openoffice  piboto

В завершение приведу небольшое демонстрационное видео для данного примера, в качестве подопытного здесь выступает мой самодельный мини-сервер.

Заключение

Добавление кнопки к Raspberry Pi, нажатие которой будет корректно и безопасно завершать работу операционной системы Raspbian GNU/Linux - это просто и удобно. Данное не сложное решение обезопасит файловые системы малинки от повреждения и потери данных, продлит срок службы карты памяти и платформе в целом.

Полезные ссылки:

Если статья оказалась полезной - помочь проекту можно тут: 👍 ПОМОЩЬ, 🎁 DONATE
1 11026 Железо
Комментарии к публикации (6):
Артем #1Артем
15 Июнь 2018 09:55

Здравствуйте!

Очень понравилась Ваша статья про создание роутера на базе малинки. Скажите, пожалуйста, планируется ли вторая часть? Интересует программная часть. Спасибо!

PS. Также очень интересно посмотреть на изготовление корпуса с кнопками, где не сами тактовые кнопки будут торчать из корпуса, а специальные "крышки" (не знаю как их назвать), для эстетики Smile

+1
ph0en1x #2ph0en1x
15 Июнь 2018 12:51

Здравствуйте! Статья по программной части для самодельного роутера получилась достаточно объемная и на 90% уже завершена. Постараюсь на следующей неделе опубликовать. 

Одна из идей насчет эстетики: кнопочную панель можно сместить в середину, чтобы колпачки кнопок были на уровне с корпусом, после этого рисуем в графическом редакторе трафарет с подписями и распечатываем его на пленке, обрезаем пленку и приклеиваем на корпус поверх кнопок - готово.

Также кнопки можно было сделать сенсорными, но это уже отдельная история. Итак уже отклонились от темы статьи "Безопасное завершение работы Raspbian...".

UPD: Статья опубликована: Самодельный роутер и мини-сервер на Raspberry Pi - Часть 2 (программы)

0
Айрат #3Айрат
27 Июнь 2018 10:13

а реально ли сделать включение и отключение распберри одной кнопкой, как на обычном пк?

0
ph0en1x #4ph0en1x
27 Июнь 2018 12:30

Айрат, реально. Для этого понадобится собрать небольшую схему управления питанием на основе микроконтроллера (МК) и нескольких транзисторов, а также написать программу для МК.

Микроконтроллер будет отвечать за ждущий режим, при нажатии на кнопку он подаст питание на малинку. При повторном нажатии на кнопку - отправит на один из GPIO сигнал что нужно завершать работу операционной системы и будет ждать примерно 30-40 сек, а потом отключит питание малинки.

Похожая штука реализована в моем самодельном роботе на основе Raspberry Pi, статьи о его изготовлении будут опубликованы позже.

0
Электронщик #5Электронщик
02 Август 2018 08:00

А если вместо МК простую логику, не вариант?

+1
ph0en1x #6ph0en1x
02 Август 2018 11:16

Первый вариант для своего робота делал именно на простой логике: использовал микросхему К561ТМ2 (одна половинка для мигания светодиодом ждущего режима, вторая - как триггер для сигналов ON-OFF), реле времени на кремниевых транзисторах и конденсаторах, ключи на полевиках IRF9610.
Плюс такого варианта в сравнении со схемой на МК - простота сборки и ничего не нужно прошивать, минус - сложно перестраивать на другие режимы и таймауты (придется менять или даже добавлять компоненты), фиксированный алгоритм работы.

0