Веб-интерфейс для управления GPIO на Raspberry Pi 5: Flask, кнопки и пароль

Хочется включать свет, вентилятор или реле с телефона, не устанавливая никаких приложений? Для этого достаточно маленького веб-сервера на самой Raspberry Pi: вы открываете страницу в браузере, нажимаете кнопку, а плата управляет GPIO. Сделать такой сервер на Python можно за полчаса с помощью микрофреймворка Flask.

В статье напишем веб-интерфейс для Raspberry Pi 5: страницу с кнопками для реле и светодиода, показом температуры процессора, программным интерфейсом (API) и защитой паролем. Потом запустим сервер как службу, которая стартует вместе с платой.


Что понадобится

  • Raspberry Pi 5 с системой (установка Raspberry Pi OS);
  • модуль реле на GPIO27 (схема подключения есть в статье про реле);
  • светодиод с резистором 330 Ом на GPIO22 (по желанию);
  • компьютер или телефон в той же локальной сети.

Если реле у вас нет, оставьте только светодиод: код подойдёт и для него.


Подготовка

Создадим каталог проекта и виртуальное окружение. Параметр --system-site-packages даёт окружению доступ к библиотеке gpiozero, которая уже установлена в системе (подробнее об окружениях: venv):

mkdir -p ~/webgpio
cd ~/webgpio
python3 -m venv --system-site-packages env
source env/bin/activate
pip install flask gunicorn

В конце установки pip может вывести красную строку с ERROR:

ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
types-flask-migrate 4.0 requires Flask-SQLAlchemy>=3.0.1, which is not installed.
Successfully installed flask-3.1.3 gunicorn-26.2.0 itsdangerous-2.2.0 werkzeug-3.1.9
Терминал Raspberry Pi 5: создание каталога webgpio и окружения env с --system-site-packages, pip install flask gunicorn; красная строка ERROR про types-flask-migrate 4.0 и Flask-SQLAlchemy, ниже Successfully installed flask-3.1.3 gunicorn-26.2.0 itsdangerous-2.2.0 werkzeug-3.1.9

Это предупреждение, а не ошибка. Установка прошла успешно, о чём говорит строка Successfully installed. Из-за --system-site-packages окружение видит и системные пакеты Python, а среди них есть types-flask-migrate, набор подсказок типов для Flask-Migrate, которому нужен ещё не установленный Flask-SQLAlchemy. Наше приложение не использует ни Flask-Migrate, ни SQLAlchemy, поэтому предупреждение можно пропустить. Если позже понадобится база данных, выполните pip install flask-sqlalchemy, и строка пропадёт.


Приложение Flask

Создайте файл app.py:

import hmac
import os
from functools import wraps

from flask import Flask, Response, jsonify, render_template_string, request
from gpiozero import CPUTemperature, LED, OutputDevice

app = Flask(__name__)

PASSWORD = os.environ.get("WEB_PASSWORD", "")

# реле с активным низким уровнем на GPIO27 и светодиод на GPIO22
DEVICES = {
    "relay": OutputDevice(27, active_high=False, initial_value=False),
    "led": LED(22),
}
cpu = CPUTemperature()

PAGE = """<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Raspberry Pi 5</title>
<style>
  body { font-family: sans-serif; max-width: 420px; margin: 2em auto; padding: 0 1em; }
  .row { display: flex; justify-content: space-between; align-items: center;
         padding: .8em 0; border-bottom: 1px solid #ddd; }
  button { font-size: 1.1em; padding: .5em 1.2em; border: 0; border-radius: 6px;
           color: #fff; background: #888; cursor: pointer; }
  button.on { background: #2e9e4f; }
</style>
</head>
<body>
<h1>Raspberry Pi 5</h1>
<div class="row"><span>Реле</span><button id="relay" onclick="toggle('relay')">…</button></div>
<div class="row"><span>Светодиод</span><button id="led" onclick="toggle('led')">…</button></div>
<div class="row"><span>Температура CPU</span><b id="temp">…</b></div>
<script>
function show(s) {
  for (const name of ['relay', 'led']) {
    const b = document.getElementById(name);
    b.textContent = s[name] ? 'Включено' : 'Выключено';
    b.className = s[name] ? 'on' : '';
  }
  document.getElementById('temp').textContent = s.cpu_temp + ' °C';
}
async function refresh() {
  const r = await fetch('/api/status');
  show(await r.json());
}
async function toggle(name) {
  const r = await fetch('/api/' + name + '/toggle', {method: 'POST'});
  show(await r.json());
}
refresh();
setInterval(refresh, 3000);
</script>
</body>
</html>
"""


def requires_auth(view):
    """Простая защита паролем (HTTP Basic). Логин любой, пароль из WEB_PASSWORD."""
    @wraps(view)
    def wrapper(*args, **kwargs):
        auth = request.authorization
        ok = (
            PASSWORD
            and auth is not None
            and hmac.compare_digest(auth.password or "", PASSWORD)
        )
        if not ok:
            return Response(
                "Нужен пароль", 401,
                {"WWW-Authenticate": 'Basic realm="Raspberry Pi"'},
            )
        return view(*args, **kwargs)
    return wrapper


def status():
    result = {name: bool(dev.value) for name, dev in DEVICES.items()}
    result["cpu_temp"] = round(cpu.temperature, 1)
    return result


@app.route("/")
@requires_auth
def index():
    return render_template_string(PAGE)


@app.route("/api/status")
@requires_auth
def api_status():
    return jsonify(status())


@app.route("/api/<name>/<action>", methods=["POST"])
@requires_auth
def api_set(name, action):
    dev = DEVICES.get(name)
    if dev is None or action not in ("on", "off", "toggle"):
        return jsonify(error="неизвестное устройство или действие"), 404
    if action == "on":
        dev.on()
    elif action == "off":
        dev.off()
    else:
        dev.toggle()
    return jsonify(status())


if __name__ == "__main__":
    # debug=False: в режиме отладки Flask запускает программу дважды,
    # и вторая копия не сможет получить доступ к тем же выводам GPIO
    app.run(host="0.0.0.0", port=5000, debug=False)

Как это устроено:

  • словарь DEVICES связывает имена («relay», «led») с объектами gpiozero; чтобы добавить новое устройство, добавьте строку в словарь;
  • requires_auth проверяет пароль и возвращает ответ 401, если он неверный. Пароль берётся из переменной окружения WEB_PASSWORD, а не из кода;
  • маршрут /api/<name>/<action> принимает только запросы POST, а не GET: так случайный переход по ссылке или предзагрузка страницы браузером не включит реле;
  • страница обновляет состояние каждые 3 секунды и показывает температуру процессора (CPUTemperature из gpiozero).

Запуск и проверка

Задайте пароль и запустите:

export WEB_PASSWORD='придумайте-пароль'
python3 app.py

Flask сообщит, на каких адресах он слушает: 127.0.0.1:5000 (сама плата) и адрес платы в локальной сети. Красная строка WARNING: This is a development server относится к встроенному серверу Flask: для опытов он подходит, а для постоянной работы ниже подключим gunicorn.

Терминал Raspberry Pi 5: nano app.py, export WEB_PASSWORD со скрытым паролем, python3 app.py; Flask сообщает Serving Flask app 'app', Debug mode: off, красное предупреждение development server, адреса 127.0.0.1:5000 и 192.168.0.122:5000

Узнайте IP-адрес платы командой hostname -I и откройте в браузере на компьютере или телефоне:

http://IP-АДРЕС:5000

Браузер спросит имя пользователя и пароль: имя введите любое, пароль — тот, что задан в WEB_PASSWORD. Появятся две кнопки и температура.

Браузер: страница Raspberry Pi 5 по адресу http://192.168.0.122:5000, строки «Реле» и «Светодиод» с серыми кнопками «Выключено» и «Температура CPU 48.5 °C»

То же самое можно сделать без браузера через API. Состояние:

curl -u user:ПАРОЛЬ http://IP-АДРЕС:5000/api/status

Включить реле:

curl -u user:ПАРОЛЬ -X POST http://IP-АДРЕС:5000/api/relay/on

Ответ приходит в формате JSON: {"cpu_temp": 48.7, "led": false, "relay": true}. Через такой API управлять платой могут скрипты, Home Assistant, Node-RED или Telegram-бот.

На снимке сначала запрос состояния ("relay":false), затем команда включения реле ("relay":true). Страница в браузере при следующем обновлении, через 3 секунды, показывает, что кнопка «Реле» стала зелёной.

Терминал: curl api/status возвращает relay false, curl -X POST api/relay/on возвращает relay true; в браузере ниже кнопка «Реле» стала зелёной «Включено», светодиод «Выключено»

Какие выводы меняются

Убедиться, что приложение действительно переключает линии GPIO, можно командой pinctrl -p. Она выводит все 40 контактов разъёма по порядку номеров: функцию, подтяжку и текущий уровень (расположение выводов и их назначение разобраны в статье про распиновку GPIO):

pinctrl -p

Строка читается так: op dl pn | lo // GPIO27 = output. Здесь op значит выход (output), dl и dh показывают, какой уровень выход держит (низкий drive low или высокий drive high), pn, pu и pd обозначают отсутствие, подтяжку к плюсу и подтяжку к земле, а hi и lo в конце сообщают реальный уровень на линии.

Сначала нажмём обе кнопки на странице («Включено») и вызовем pinctrl -p:

Терминал Raspberry Pi 5: вывод pinctrl -p, контакт 13 GPIO27 op dl pn lo output, контакт 15 GPIO22 op dh pn hi output; справа страница управления с зелёными кнопками «Включено» у реле и светодиода

Затем выключим обе кнопки («Выключено») и повторим команду:

Терминал Raspberry Pi 5: вывод pinctrl -p, контакт 13 GPIO27 op dh pn hi output, контакт 15 GPIO22 op dl pn lo output; справа страница управления с серыми кнопками «Выключено» у реле и светодиода

Между двумя снимками изменились ровно две строки, остальные контакты остались как были:

КонтактЛинияУстройствоВключеноВыключено
13GPIO27релеop dl pn | lo (низкий уровень)op dh pn | hi (высокий уровень)
15GPIO22светодиодop dh pn | hi (высокий уровень)op dl pn | lo (низкий уровень)

У светодиода всё как обычно: включено — высокий уровень на выводе. У реле наоборот: наш модуль включается низким уровнем, поэтому в программе указано active_high=False, а на странице «Включено» соответствует lo. Если у вашего модуля реле включается высоким уровнем, уберите этот параметр.

Остальные строки в выводе (SPI, UART, I2C, GPIO7 и GPIO8 в режиме op dh) относятся к системе и нашим приложением не меняются.


Безопасность

Пароль без шифрования. HTTP Basic без HTTPS передаёт пароль открытым текстом (в кодировке base64, это не шифрование). В домашней сети это приемлемо для опытов, но не открывайте этот сервер в интернет и не пробрасывайте порт на роутере. Для доступа из любого места используйте VPN WireGuard: подключаетесь к домашней сети и открываете страницу как обычно. Не используйте такой интерфейс для управления опасными нагрузками.

Ещё несколько правил:

  • пароль храните в переменной окружения или в файле с правами chmod 600, а не в коде;
  • не передавайте в API произвольные номера GPIO или команды: разрешайте только заранее заданные устройства (как в словаре DEVICES);
  • встроенный сервер Flask (app.run) предназначен для разработки; для постоянной работы используйте gunicorn, как показано дальше.

Запуск при включении платы

Запускаем через gunicorn

gunicorn — рабочий HTTP-сервер для Python-приложений. Важно запускать его с одним рабочим процессом (--workers 1): GPIO-линиями может владеть только один процесс, поэтому несколько процессов сразу будут конфликтовать. Параллельные запросы обработают потоки (--threads 4).

Проверка вручную (из каталога ~/webgpio, с активным окружением):

gunicorn --workers 1 --threads 4 --bind 0.0.0.0:8000 app:app

Если в соседнем окне ещё работает python3 app.py, остановите его сочетанием Ctrl+C: обе программы не смогут одновременно владеть линиями GPIO. Откройте http://IP-АДРЕС:8000. Браузер снова спросит имя пользователя и пароль:

Окно браузера «This site is asking you to sign in» для http://192.168.0.122:8000 с полем Username pi, скрытым паролем и кнопками Sign in и Cancel

После входа появится та же страница, что и раньше. В терминале gunicorn пишет, что запущен рабочий процесс типа gthread (потоки), и слушает порт 8000. Строки Handling signal: winch появляются, когда меняется размер окна терминала, их можно пропустить.

Терминал Raspberry Pi 5: gunicorn --workers 1 --threads 4 --bind 0.0.0.0:8000 app:app, Starting gunicorn 26.2.0, Listening at http://0.0.0.0:8000, Using worker gthread, Booting worker with pid; в браузере страница Raspberry Pi 5, реле «Включено», светодиод «Выключено», температура CPU 47.4 °C

Служба systemd

Пароль положите в отдельный файл, доступный только владельцу:

sudo nano /etc/webgpio.env
WEB_PASSWORD=придумайте-пароль
sudo chmod 600 /etc/webgpio.env

Создайте файл службы:

sudo nano /etc/systemd/system/webgpio.service
[Unit]
Description=Web interface for Raspberry Pi GPIO
After=network-online.target
Wants=network-online.target

[Service]
User=pi
WorkingDirectory=/home/pi/webgpio
EnvironmentFile=/etc/webgpio.env
ExecStart=/home/pi/webgpio/env/bin/gunicorn --workers 1 --threads 4 --bind 0.0.0.0:8000 app:app
Restart=on-failure

[Install]
WantedBy=multi-user.target

Если ваш пользователь называется не pi, замените имя и пути. Включите и запустите службу:

sudo systemctl daemon-reload
sudo systemctl enable --now webgpio.service
sudo systemctl status webgpio.service

Подробнее о службах и разборе ошибок читайте в статье про автозапуск через systemd.


Что можно добавить

  • Расписание: запускать включение реле по таймеру через cron или systemd-таймер;
  • Датчики: показывать температуру и влажность с DHT22 на той же странице;
  • Управление сервоприводом: ползунок угла для сервопривода;
  • Датчик движения: статус PIR-датчика на странице;
  • HTTPS: поставить перед приложением обратный прокси Nginx, как в статье про LEMP, и получить сертификат.

Если что-то не так

СимптомЧто проверить
Браузер не открывает страницуIP платы, порт (5000 для app.py, 8000 для gunicorn), что компьютер в той же сети
Всегда 401 «Нужен пароль»Переменная WEB_PASSWORD не задана (пустой пароль не принимается) или пароль введён неверно
GPIOPinInUse или Device or resource busyДругая копия программы уже владеет линией: завершите её (pgrep -af app.py) или остановите службу
Служба не запускаетсяjournalctl -u webgpio -e: путь к gunicorn, имя пользователя, права на /etc/webgpio.env
Кнопка работает, но реле не включаетсяПроверьте схему и логику модуля (реле на Raspberry Pi 5)

Что дальше

  • Реле на Raspberry Pi 5 — подключение и безопасность;
  • Node-RED на Raspberry Pi 5 — визуальные сценарии поверх такого API;
  • Telegram-бот на Raspberry Pi — управление командами из мессенджера.

Материалы

Оставьте комментарий

Отправляя комментарий, вы соглашаетесь с обработкой ваших данных (имя, адрес электронной почты, текст комментария) в соответствии с политикой конфиденциальности.